using AngleSharp.Dom; using XFESystem.Desktop; namespace XFESystem.Browser; /// /// Immutable paint-only publication. No DOM node, layout box, text-metrics /// provider or live canvas backing store crosses the worker/UI boundary. /// public sealed class BrowserPaintSnapshot { private readonly IReadOnlyList commands; private readonly BrowserCanvasSurface?[] surfaces; private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands, BrowserCanvasSurface?[] surfaces, double documentWidth, double documentHeight, long canvasBytes) { Viewport = viewport; this.commands = Array.AsReadOnly(commands); this.surfaces = surfaces; DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes; } public BrowserViewport Viewport { get; } public IReadOnlyList Commands => commands; public double DocumentWidth { get; } public double DocumentHeight { get; } public long CanvasBytes { get; } /// Called only by the current exclusive page owner, before release publication. public static BrowserPaintSnapshot Capture(BrowserDisplayList list, Func? sourceSurfaces = null, int maximumCommands = 100_000, long maximumCanvasPixels = 16_777_216) { 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 copied = new Dictionary(); 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 }; if (source.Kind != BrowserDrawKind.Canvas || source.Node is null || sourceSurfaces?.Invoke(source.Node) is not { } surface) 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, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint))); } 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]; public void Paint(IDesktopCanvas canvas, DesktopBounds viewport) => BrowserRasterPainter.Paint(canvas, viewport, this); } public static partial class BrowserRasterPainter { /// Only the frozen snapshot and UI-owned renderer are accessed here. public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserPaintSnapshot snapshot) { ArgumentNullException.ThrowIfNull(snapshot); var web = canvas as IDesktopWebCanvas; var rounded = new BrowserClipRasterCanvas(canvas); var clips = new Stack(); var clip = new BrowserRect(viewport.X, viewport.Y, viewport.Width, viewport.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; } web?.PushClip(viewport); canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height); try { for (int index = 0; index < snapshot.Commands.Count; index++) { var item = snapshot.Commands[index]; if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue; double dx = viewport.X - snapshot.Viewport.ScrollX, dy = viewport.Y - snapshot.Viewport.ScrollY; 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 visible = r.Intersect(clip); if (visible.Width <= 0 || visible.Height <= 0) continue; IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas; if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible); else if (item.Kind == BrowserDrawKind.Canvas) { if (snapshot.SurfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); } else if (item.Kind == BrowserDrawKind.Text) { 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 && rounded.Active && canvas is IDesktopCoverageCanvas) rounded.WebText(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 (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); } } } catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; } while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); } }