using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// Immutable paint-only publication. No DOM node, layout box, text-metrics
/// provider or live canvas backing store crosses the worker/UI boundary.
/// </summary>
public sealed class BrowserPaintSnapshot
{
private readonly IReadOnlyList<BrowserDrawCommand> commands;
private readonly BrowserCanvasSurface?[] surfaces;
private readonly IDesktopPreparedWebText?[] text;
private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands,
BrowserCanvasSurface?[] surfaces, IDesktopPreparedWebText?[] text, double documentWidth, double documentHeight, long canvasBytes, bool hasPreparedText)
{
Viewport = viewport;
this.commands = Array.AsReadOnly(commands);
this.surfaces = surfaces;
this.text = text;
DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes;
HasPreparedText = hasPreparedText;
}
public BrowserViewport Viewport { get; }
public IReadOnlyList<BrowserDrawCommand> Commands => commands;
public double DocumentWidth { get; }
public double DocumentHeight { get; }
public long CanvasBytes { get; }
public int PreparedTextCount => text.Count(item => item is not null);
public bool HasPreparedText { get; }
/// <summary>Called only by the current exclusive page owner, before release publication.</summary>
public static BrowserPaintSnapshot Capture(BrowserDisplayList list,
Func<INode, BrowserCanvasSurface?>? sourceSurfaces = null,
int maximumCommands = 100_000, long maximumCanvasPixels = 16_777_216,
IDesktopWebTextPreparationCanvas? textPreparation = null, Func<string, BrowserCanvasSurface?>? sourceImages = null)
{
ArgumentNullException.ThrowIfNull(list);
if (maximumCommands < 1 || maximumCommands > 100_000 || maximumCanvasPixels < 0 || maximumCanvasPixels > 16_777_216)
throw new ArgumentOutOfRangeException(nameof(maximumCommands), "Invalid paint snapshot limits.");
if (list.Commands.Count > maximumCommands) throw new BrowserLimitException("The paint snapshot command limit was exceeded.");
var commands = new BrowserDrawCommand[list.Commands.Count];
var surfaces = new BrowserCanvasSurface?[commands.Length];
var text = new IDesktopPreparedWebText?[commands.Length];
var copied = new Dictionary<BrowserCanvasSurface, BrowserCanvasSurface>();
long pixels = 0;
for (int index = 0; index < commands.Length; index++)
{
var source = list.Commands[index];
// Strings and value geometry are immutable. Every nested mutable
// list is independently copied; the node is deliberately dropped.
commands[index] = source with
{
Node = null,
Decoration = CopyDecoration(source.Decoration),
RoundedClip = source.RoundedClip is { } rounded ? rounded with { } : null
};
// Only visible runs need native glyph coverage. Long documents do
// not create a full-document raster or a contiguous page bitmap.
if (textPreparation is not null && source.Kind == BrowserDrawKind.Text && source.Opacity > 0 &&
source.Text.Length != 0 && (source.Transform ?? BrowserAffineTransform.Identity).Bounds(source.Bounds).Intersect(new(list.Viewport.ScrollX, list.Viewport.ScrollY,
list.Viewport.Width, list.Viewport.Height)) is { Width: > 0, Height: > 0 })
text[index] = textPreparation.PrepareWebText(source.Text, source.Font.Size, source.Font.Family, source.Font.Weight, source.Opacity);
BrowserCanvasSurface? surface = source.Kind == BrowserDrawKind.Image ? sourceImages?.Invoke(source.ImageSource) :
source.Kind == BrowserDrawKind.Canvas && source.Node is not null ? sourceSurfaces?.Invoke(source.Node) : null;
if (surface is null) continue;
if (!copied.TryGetValue(surface, out var frozen))
{
long size = checked((long)surface.Width * surface.Height);
if (surface.Width < 0 || surface.Height < 0 || size > maximumCanvasPixels - pixels)
throw new BrowserLimitException("The paint snapshot canvas pixel limit was exceeded.");
frozen = surface.CopyForPaintSnapshot();
copied.Add(surface, frozen); pixels += size;
}
surfaces[index] = frozen;
}
return new(list.Viewport, commands, surfaces, text, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint)), textPreparation is not null);
}
private static BrowserBoxDecoration? CopyDecoration(BrowserBoxDecoration? source) => source is null ? null : source with
{
Shadows = Array.AsReadOnly(source.Shadows.Select(shadow => shadow with { }).ToArray()),
Gradients = Array.AsReadOnly(source.Gradients.Select(gradient => gradient with
{
Stops = Array.AsReadOnly(gradient.Stops.Select(stop => stop with { }).ToArray())
}).ToArray())
};
internal BrowserCanvasSurface? SurfaceAt(int commandIndex) => surfaces[commandIndex];
internal IDesktopPreparedWebText? TextAt(int commandIndex) => text[commandIndex];
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport) => BrowserRasterPainter.Paint(canvas, viewport, this);
}
public static partial class BrowserRasterPainter
{
/// <summary>Only the frozen snapshot and UI-owned renderer are accessed here.</summary>
public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserPaintSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
PaintCore(canvas, viewport, snapshot.Viewport, snapshot.Commands, snapshot.SurfaceAt, snapshot.TextAt, snapshot.HasPreparedText);
}
private static void PaintCore(IDesktopCanvas canvas, DesktopBounds viewport, BrowserViewport pageViewport,
IReadOnlyList<BrowserDrawCommand> commands, Func<int, BrowserCanvasSurface?> surfaceAt,
Func<int, IDesktopPreparedWebText?>? textAt, bool hasPreparedText)
{
var web = canvas as IDesktopWebCanvas;
var rounded = new BrowserClipRasterCanvas(canvas);
var clips = new Stack<BrowserRect>();
var clip = new BrowserRect(viewport.X, viewport.Y, viewport.Width, viewport.Height);
var preparedViewport = hasPreparedText ? viewport with
{ Width = Math.Min(viewport.Width, (int)Math.Floor(pageViewport.Width)), Height = Math.Min(viewport.Height, (int)Math.Floor(pageViewport.Height)) } : viewport;
clip = clip.Intersect(new(preparedViewport.X, preparedViewport.Y, preparedViewport.Width, preparedViewport.Height));
if (canvas is IDesktopWebFrameCache frameCache)
{
var scene = frameCache.WebPaintClip;
clip = clip.Intersect(new(scene.X, scene.Y, scene.Width, scene.Height));
if (clip.Width <= 0 || clip.Height <= 0) return;
}
double dx = viewport.X - pageViewport.ScrollX, dy = viewport.Y - pageViewport.ScrollY;
web?.PushClip(preparedViewport);
canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height);
try
{
for (int index = 0; index < commands.Count; index++)
{
var item = commands[index];
if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
var r = item.Bounds.Translate(dx, dy);
if (item.Kind == BrowserDrawKind.PushClip)
{ clips.Push(clip); clip = clip.Intersect(r); rounded.Push(item.RoundedClip, dx, dy); web?.PushClip(ToBounds(clip)); continue; }
if (item.Kind == BrowserDrawKind.PopClip)
{ if (clips.TryPop(out var old)) { clip = old; rounded.Pop(); web?.PopClip(); } continue; }
var matrix = item.Transform ?? BrowserAffineTransform.Identity;
var paintedBounds = matrix.Bounds(item.Bounds).Translate(dx, dy);
var visible = paintedBounds.Intersect(clip);
if (visible.Width <= 0 || visible.Height <= 0) continue;
IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas;
BrowserAffineRasterCanvas? transformed = null;
if (!matrix.IsIdentity)
{
var screenMatrix = BrowserAffineTransform.Translation(dx, dy) * matrix * BrowserAffineTransform.Translation(-dx, -dy);
transformed = new(target, r, clip, screenMatrix);
target = transformed; visible = transformed.SourceClip;
}
if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible);
else if (item.Kind is BrowserDrawKind.Canvas or BrowserDrawKind.Image)
{ if (surfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); }
else if (item.Kind == BrowserDrawKind.Text)
{
if (textAt?.Invoke(index) is { } prepared && prepared.ScalePercent == canvas.ScalePercent && target is IDesktopCoverageCanvas coverageText)
prepared.Paint(coverageText, item.Color, r.X, r.Y + (r.Height - prepared.LineHeight) / 2);
else
{
double leading = web is null ? 0 : (r.Height - web.MeasureWebLineHeight(item.Font.Size, item.Font.Family, item.Font.Weight)) / 2;
if (web is not null && target is IDesktopCoverageCanvas && (transformed is not null || rounded.Active))
PaintTextMask((IDesktopCoverageCanvas)target, web, item, r.Translate(0, leading));
else if (web is not null) web.WebText(item.Text, item.Color, r.X, r.Y + leading, item.Font.Size, item.Font.Family, item.Font.Weight, item.Opacity);
else if (transformed is null && visible == r) canvas.Text(item.Text, item.Color, (int)r.X, (int)r.Y);
}
}
else
{
var b = ToBounds(visible);
if (item.Opacity < 1 && target is IDesktopCoverageCanvas coverage)
{
int left = (int)Math.Floor(visible.X * canvas.ScalePercent / 100d), top = (int)Math.Floor(visible.Y * canvas.ScalePercent / 100d);
int width = Math.Max(1, (int)Math.Ceiling(visible.Width * canvas.ScalePercent / 100d)), height = Math.Max(1, (int)Math.Ceiling(visible.Height * canvas.ScalePercent / 100d));
var row = new byte[width]; Array.Fill(row, (byte)Math.Clamp(item.Opacity * 255, 0, 255));
for (int y = 0; y < height; y++) coverage.BlendCoverage(item.Color, left, top + y, width, 1, row);
}
else target.Fill(item.Color, b.X, b.Y, b.Width, b.Height);
}
transformed?.Flush();
}
}
catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; }
while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip();
}
private static void PaintTextMask(IDesktopCoverageCanvas target, IDesktopWebCanvas web, BrowserDrawCommand command, BrowserRect bounds)
{
double scale = target.ScalePercent / 100d;
var mask = web.RasterizeWebText(command.Text, command.Font.Size * scale, command.Font.Family, command.Font.Weight);
if (command.Opacity < 1)
for (int index = 0; index < mask.Alpha.Length; index++) mask.Alpha[index] = (byte)Math.Clamp(Math.Round(mask.Alpha[index] * command.Opacity), 0, 255);
target.BlendCoverage(command.Color, (int)Math.Round(bounds.X * scale), (int)Math.Round(bounds.Y * scale), mask.Width, mask.Height, mask.Alpha);
}
}
using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser;
/// <summary>
/// Immutable paint-only publication. No DOM node, layout box, text-metrics
/// provider or live canvas backing store crosses the worker/UI boundary.
/// </summary>
public sealed class BrowserPaintSnapshot
{
private readonly IReadOnlyList<BrowserDrawCommand> commands;
private readonly BrowserCanvasSurface?[] surfaces;
private readonly IDesktopPreparedWebText?[] text;
private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands,
BrowserCanvasSurface?[] surfaces, IDesktopPreparedWebText?[] text, double documentWidth, double documentHeight, long canvasBytes, bool hasPreparedText)
{
Viewport = viewport;
this.commands = Array.AsReadOnly(commands);
this.surfaces = surfaces;
this.text = text;
DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes;
HasPreparedText = hasPreparedText;
}
public BrowserViewport Viewport { get; }
public IReadOnlyList<BrowserDrawCommand> Commands => commands;
public double DocumentWidth { get; }
public double DocumentHeight { get; }
public long CanvasBytes { get; }
public int PreparedTextCount => text.Count(item => item is not null);
public bool HasPreparedText { get; }
/// <summary>Called only by the current exclusive page owner, before release publication.</summary>
public static BrowserPaintSnapshot Capture(BrowserDisplayList list,
Func<INode, BrowserCanvasSurface?>? sourceSurfaces = null,
int maximumCommands = 100_000, long maximumCanvasPixels = 16_777_216,
IDesktopWebTextPreparationCanvas? textPreparation = null, Func<string, BrowserCanvasSurface?>? sourceImages = null)
{
ArgumentNullException.ThrowIfNull(list);
if (maximumCommands < 1 || maximumCommands > 100_000 || maximumCanvasPixels < 0 || maximumCanvasPixels > 16_777_216)
throw new ArgumentOutOfRangeException(nameof(maximumCommands), "Invalid paint snapshot limits.");
if (list.Commands.Count > maximumCommands) throw new BrowserLimitException("The paint snapshot command limit was exceeded.");
var commands = new BrowserDrawCommand[list.Commands.Count];
var surfaces = new BrowserCanvasSurface?[commands.Length];
var text = new IDesktopPreparedWebText?[commands.Length];
var copied = new Dictionary<BrowserCanvasSurface, BrowserCanvasSurface>();
long pixels = 0;
for (int index = 0; index < commands.Length; index++)
{
var source = list.Commands[index];
// Strings and value geometry are immutable. Every nested mutable
// list is independently copied; the node is deliberately dropped.
commands[index] = source with
{
Node = null,
Decoration = CopyDecoration(source.Decoration),
RoundedClip = source.RoundedClip is { } rounded ? rounded with { } : null
};
// Only visible runs need native glyph coverage. Long documents do
// not create a full-document raster or a contiguous page bitmap.
if (textPreparation is not null && source.Kind == BrowserDrawKind.Text && source.Opacity > 0 &&
source.Text.Length != 0 && (source.Transform ?? BrowserAffineTransform.Identity).Bounds(source.Bounds).Intersect(new(list.Viewport.ScrollX, list.Viewport.ScrollY,
list.Viewport.Width, list.Viewport.Height)) is { Width: > 0, Height: > 0 })
text[index] = textPreparation.PrepareWebText(source.Text, source.Font.Size, source.Font.Family, source.Font.Weight, source.Opacity);
BrowserCanvasSurface? surface = source.Kind == BrowserDrawKind.Image ? sourceImages?.Invoke(source.ImageSource) :
source.Kind == BrowserDrawKind.Canvas && source.Node is not null ? sourceSurfaces?.Invoke(source.Node) : null;
if (surface is null) continue;
if (!copied.TryGetValue(surface, out var frozen))
{
long size = checked((long)surface.Width * surface.Height);
if (surface.Width < 0 || surface.Height < 0 || size > maximumCanvasPixels - pixels)
throw new BrowserLimitException("The paint snapshot canvas pixel limit was exceeded.");
frozen = surface.CopyForPaintSnapshot();
copied.Add(surface, frozen); pixels += size;
}
surfaces[index] = frozen;
}
return new(list.Viewport, commands, surfaces, text, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint)), textPreparation is not null);
}
private static BrowserBoxDecoration? CopyDecoration(BrowserBoxDecoration? source) => source is null ? null : source with
{
Shadows = Array.AsReadOnly(source.Shadows.Select(shadow => shadow with { }).ToArray()),
Gradients = Array.AsReadOnly(source.Gradients.Select(gradient => gradient with
{
Stops = Array.AsReadOnly(gradient.Stops.Select(stop => stop with { }).ToArray())
}).ToArray())
};
internal BrowserCanvasSurface? SurfaceAt(int commandIndex) => surfaces[commandIndex];
internal IDesktopPreparedWebText? TextAt(int commandIndex) => text[commandIndex];
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport) => BrowserRasterPainter.Paint(canvas, viewport, this);
}
public static partial class BrowserRasterPainter
{
/// <summary>Only the frozen snapshot and UI-owned renderer are accessed here.</summary>
public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserPaintSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
PaintCore(canvas, viewport, snapshot.Viewport, snapshot.Commands, snapshot.SurfaceAt, snapshot.TextAt, snapshot.HasPreparedText);
}
private static void PaintCore(IDesktopCanvas canvas, DesktopBounds viewport, BrowserViewport pageViewport,
IReadOnlyList<BrowserDrawCommand> commands, Func<int, BrowserCanvasSurface?> surfaceAt,
Func<int, IDesktopPreparedWebText?>? textAt, bool hasPreparedText)
{
var web = canvas as IDesktopWebCanvas;
var rounded = new BrowserClipRasterCanvas(canvas);
var clips = new Stack<BrowserRect>();
var clip = new BrowserRect(viewport.X, viewport.Y, viewport.Width, viewport.Height);
var preparedViewport = hasPreparedText ? viewport with
{ Width = Math.Min(viewport.Width, (int)Math.Floor(pageViewport.Width)), Height = Math.Min(viewport.Height, (int)Math.Floor(pageViewport.Height)) } : viewport;
clip = clip.Intersect(new(preparedViewport.X, preparedViewport.Y, preparedViewport.Width, preparedViewport.Height));
if (canvas is IDesktopWebFrameCache frameCache)
{
var scene = frameCache.WebPaintClip;
clip = clip.Intersect(new(scene.X, scene.Y, scene.Width, scene.Height));
if (clip.Width <= 0 || clip.Height <= 0) return;
}
double dx = viewport.X - pageViewport.ScrollX, dy = viewport.Y - pageViewport.ScrollY;
web?.PushClip(preparedViewport);
canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height);
try
{
for (int index = 0; index < commands.Count; index++)
{
var item = commands[index];
if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
var r = item.Bounds.Translate(dx, dy);
if (item.Kind == BrowserDrawKind.PushClip)
{ clips.Push(clip); clip = clip.Intersect(r); rounded.Push(item.RoundedClip, dx, dy); web?.PushClip(ToBounds(clip)); continue; }
if (item.Kind == BrowserDrawKind.PopClip)
{ if (clips.TryPop(out var old)) { clip = old; rounded.Pop(); web?.PopClip(); } continue; }
var matrix = item.Transform ?? BrowserAffineTransform.Identity;
var paintedBounds = matrix.Bounds(item.Bounds).Translate(dx, dy);
var visible = paintedBounds.Intersect(clip);
if (visible.Width <= 0 || visible.Height <= 0) continue;
IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas;
BrowserAffineRasterCanvas? transformed = null;
if (!matrix.IsIdentity)
{
var screenMatrix = BrowserAffineTransform.Translation(dx, dy) * matrix * BrowserAffineTransform.Translation(-dx, -dy);
transformed = new(target, r, clip, screenMatrix);
target = transformed; visible = transformed.SourceClip;
}
if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible);
else if (item.Kind is BrowserDrawKind.Canvas or BrowserDrawKind.Image)
{ if (surfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); }
else if (item.Kind == BrowserDrawKind.Text)
{
if (textAt?.Invoke(index) is { } prepared && prepared.ScalePercent == canvas.ScalePercent && target is IDesktopCoverageCanvas coverageText)
prepared.Paint(coverageText, item.Color, r.X, r.Y + (r.Height - prepared.LineHeight) / 2);
else
{
double leading = web is null ? 0 : (r.Height - web.MeasureWebLineHeight(item.Font.Size, item.Font.Family, item.Font.Weight)) / 2;
if (web is not null && target is IDesktopCoverageCanvas && (transformed is not null || rounded.Active))
PaintTextMask((IDesktopCoverageCanvas)target, web, item, r.Translate(0, leading));
else if (web is not null) web.WebText(item.Text, item.Color, r.X, r.Y + leading, item.Font.Size, item.Font.Family, item.Font.Weight, item.Opacity);
else if (transformed is null && visible == r) canvas.Text(item.Text, item.Color, (int)r.X, (int)r.Y);
}
}
else
{
var b = ToBounds(visible);
if (item.Opacity < 1 && target is IDesktopCoverageCanvas coverage)
{
int left = (int)Math.Floor(visible.X * canvas.ScalePercent / 100d), top = (int)Math.Floor(visible.Y * canvas.ScalePercent / 100d);
int width = Math.Max(1, (int)Math.Ceiling(visible.Width * canvas.ScalePercent / 100d)), height = Math.Max(1, (int)Math.Ceiling(visible.Height * canvas.ScalePercent / 100d));
var row = new byte[width]; Array.Fill(row, (byte)Math.Clamp(item.Opacity * 255, 0, 255));
for (int y = 0; y < height; y++) coverage.BlendCoverage(item.Color, left, top + y, width, 1, row);
}
else target.Fill(item.Color, b.X, b.Y, b.Width, b.Height);
}
transformed?.Flush();
}
}
catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; }
while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip();
}
private static void PaintTextMask(IDesktopCoverageCanvas target, IDesktopWebCanvas web, BrowserDrawCommand command, BrowserRect bounds)
{
double scale = target.ScalePercent / 100d;
var mask = web.RasterizeWebText(command.Text, command.Font.Size * scale, command.Font.Family, command.Font.Weight);
if (command.Opacity < 1)
for (int index = 0; index < mask.Alpha.Length; index++) mask.Alpha[index] = (byte)Math.Clamp(Math.Round(mask.Alpha[index] * command.Opacity), 0, 255);
target.BlendCoverage(command.Color, (int)Math.Round(bounds.X * scale), (int)Math.Round(bounds.Y * scale), mask.Width, mask.Height, mask.Alpha);
}
}