XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using Jint;
using Jint.Native;
using Jint.Native.Object;

namespace XFESystem.Browser;

public sealed partial class DomBridge
{
    private sealed class CanvasState(BrowserCanvasSurface surface, ObjectInstance context)
    { public BrowserCanvasSurface Surface = surface; public ObjectInstance Context = context; public double TranslateX, TranslateY; public double ScaleX = 1, ScaleY = 1; public Stack<(double X, double Y, double ScaleX, double ScaleY, string Fill, string Font, double Alpha)> Saved = []; }
    private readonly Dictionary<IElement, CanvasState> canvases = new(ReferenceEqualityComparer.Instance);
    public BrowserCanvasSurface? GetCanvasSurface(INode node) => node is IElement element && canvases.TryGetValue(element, out var state) ? state.Surface : null;
    public bool TryCanvas(INode node, out BrowserCanvasSurface surface)
    {
        var found = GetCanvasSurface(node); surface = found!; return found is not null;
    }
    private void InstallCanvas(ObjectInstance wrapper, IElement element)
    {
        Property(wrapper, "width", () => CanvasDimension(element, "width", 300), value => ResizeCanvas(element, "width", NumberIndex(value)));
        Property(wrapper, "height", () => CanvasDimension(element, "height", 150), value => ResizeCanvas(element, "height", NumberIndex(value)));
        Function(wrapper, "getContext", args => Argument(args, 0) == "2d" ? Canvas(element, wrapper).Context : JsValue.Null);
    }
    private CanvasState Canvas(IElement element, ObjectInstance wrapper)
    {
        if (canvases.TryGetValue(element, out var old)) return old;
        var width = CanvasDimension(element, "width", 300); var height = CanvasDimension(element, "height", 150); CheckCanvasBudget(element, width, height);
        var context = new BrowserHostObject(engine); var state = new CanvasState(new(width, height), context); canvases.Add(element, state);
        Data(context, "canvas", wrapper); Data(context, "backingStorePixelRatio", 1); Data(context, "fillStyle", "#000000", true); Data(context, "strokeStyle", "#000000", true);
        Data(context, "globalAlpha", 1, true); Data(context, "font", "10px sans-serif", true); Data(context, "textBaseline", "alphabetic", true); Data(context, "textAlign", "start", true);
        Function(context, "fillRect", args => { FillCanvas(state, args, false); CanvasChanged(element, state); return JsValue.Undefined; });
        Function(context, "clearRect", args => { FillCanvas(state, args, true); CanvasChanged(element, state); return JsValue.Undefined; });
        Function(context, "measureText", args =>
        {
            var text = Argument(args, 0); var font = CanvasFont(context.Get("font")); var measured = host.MeasureText?.Invoke(text, font) ?? new BrowserFallbackTextMetrics().Measure(text, font);
            var metrics = new BrowserHostObject(engine); Data(metrics, "width", measured); Data(metrics, "actualBoundingBoxLeft", 0); Data(metrics, "actualBoundingBoxRight", measured);
            Data(metrics, "actualBoundingBoxAscent", font.Size * .8); Data(metrics, "actualBoundingBoxDescent", font.Size * .2); return metrics;
        });
        Function(context, "fillText", args => { FillCanvasText(state, args); CanvasChanged(element, state); return JsValue.Undefined; });
        Function(context, "strokeText", _ => throw new NotSupportedException("Canvas text rasterization is not available on this platform."));
        Function(context, "getImageData", args => ImageData(state.Surface, NumberIndexSigned(Value(args, 0)), NumberIndexSigned(Value(args, 1)), NumberIndex(Value(args, 2)), NumberIndex(Value(args, 3))));
        Function(context, "createImageData", args => ImageData(null, 0, 0, NumberIndex(Value(args, 0)), NumberIndex(Value(args, 1))));
        Function(context, "putImageData", args => { PutImageData(state.Surface, Value(args, 0), NumberIndexSigned(Value(args, 1)), NumberIndexSigned(Value(args, 2))); CanvasChanged(element, state); return JsValue.Undefined; });
        Function(context, "translate", args => { state.TranslateX += Number(Value(args, 0)); state.TranslateY += Number(Value(args, 1)); return JsValue.Undefined; });
        Function(context, "scale", args => { state.ScaleX *= Number(Value(args, 0)); state.ScaleY *= Number(Value(args, 1)); return JsValue.Undefined; });
        Function(context, "setTransform", args =>
        {
            if (Number(Value(args, 1)) != 0 || Number(Value(args, 2)) != 0) throw new NotSupportedException("Rotated/skewed canvas transforms are not supported.");
            state.ScaleX = Number(Value(args, 0)); state.ScaleY = Number(Value(args, 3)); state.TranslateX = Number(Value(args, 4)); state.TranslateY = Number(Value(args, 5)); return JsValue.Undefined;
        });
        Function(context, "resetTransform", _ => { state.ScaleX = state.ScaleY = 1; state.TranslateX = state.TranslateY = 0; return JsValue.Undefined; });
        Function(context, "save", _ =>
        {
            if (state.Saved.Count >= 64) throw new BrowserLimitException("Canvas state stack limit reached.");
            state.Saved.Push((state.TranslateX, state.TranslateY, state.ScaleX, state.ScaleY, context.Get("fillStyle").ToString(), context.Get("font").ToString(), Number(context.Get("globalAlpha")))); return JsValue.Undefined;
        });
        Function(context, "restore", _ =>
        {
            if (state.Saved.TryPop(out var saved)) { state.TranslateX = saved.X; state.TranslateY = saved.Y; state.ScaleX = saved.ScaleX; state.ScaleY = saved.ScaleY; context.Set("fillStyle", saved.Fill, context); context.Set("font", saved.Font, context); context.Set("globalAlpha", saved.Alpha, context); } return JsValue.Undefined;
        });
        return state;
    }
    private void FillCanvas(CanvasState state, JsValue[] args, bool clear)
    {
        var color = CssValue.Color(state.Context.Get("fillStyle").ToString(), new(0)); var alpha = Math.Clamp(Number(state.Context.Get("globalAlpha")), 0, 1) * color.Alpha;
        var argb = (uint)Math.Round(alpha * 255) << 24 | color.Rgb;
        state.Surface.Fill(state.TranslateX + Number(Value(args, 0)) * state.ScaleX, state.TranslateY + Number(Value(args, 1)) * state.ScaleY, Number(Value(args, 2)) * state.ScaleX, Number(Value(args, 3)) * state.ScaleY, argb, clear);
    }
    private void FillCanvasText(CanvasState state, JsValue[] args)
    {
        if (host.RasterizeText is null) throw new NotSupportedException("Canvas text rasterization is not available on this platform; CSS text rendering remains available.");
        var text = Argument(args, 0); var font = CanvasFont(state.Context.Get("font"));
        if (text.Length > 4096) throw new BrowserLimitException("Canvas text exceeds its rasterization budget.");
        var measured = host.MeasureText?.Invoke(text, font) ?? new BrowserFallbackTextMetrics().Measure(text, font);
        if (measured > 4096) throw new BrowserLimitException("Canvas text exceeds its rasterization width budget.");
        var bitmap = host.RasterizeText(text, font);
        if (bitmap.Width < 0 || bitmap.Height < 0 || (long)bitmap.Width * bitmap.Height > 1_048_576 || bitmap.Alpha.Length != (long)bitmap.Width * bitmap.Height)
            throw new BrowserLimitException("Canvas font rasterizer returned an invalid or oversized mask.");
        var x = Number(Value(args, 1)); var y = Number(Value(args, 2));
        var align = state.Context.Get("textAlign").ToString(); if (align is "right" or "end") x -= measured; else if (align == "center") x -= measured / 2;
        var baseline = state.Context.Get("textBaseline").ToString();
        if (baseline is "bottom" or "ideographic") y -= bitmap.Height; else if (baseline == "middle") y -= bitmap.Height / 2d; else if (baseline == "alphabetic") y -= font.Size * .8;
        var color = CssValue.Color(state.Context.Get("fillStyle").ToString(), new(0));
        var alpha = Math.Clamp(Number(state.Context.Get("globalAlpha")), 0, 1) * color.Alpha;
        var sx = state.ScaleX;
        if (args.Length > 3 && Number(Value(args, 3)) > 0 && measured > Number(Value(args, 3))) sx *= Number(Value(args, 3)) / measured;
        state.Surface.BlendMask(bitmap.Alpha, bitmap.Width, bitmap.Height, state.TranslateX + x * state.ScaleX, state.TranslateY + y * state.ScaleY, sx, state.ScaleY, color.Rgb, alpha);
    }
    private void CanvasChanged(IElement element, CanvasState state) { host.Mutated?.Invoke(); host.CanvasChanged?.Invoke(element, state.Surface); }
    private void ResizeCanvas(IElement element, string dimension, int value)
    {
        var width = dimension == "width" ? value : CanvasDimension(element, "width", 300); var height = dimension == "height" ? value : CanvasDimension(element, "height", 150);
        CheckCanvasBudget(element, width, height); SetAttribute(element, dimension, value.ToString(CultureInfo.InvariantCulture));
    }
    private void CanvasAttributeChanged(IElement element, string attribute)
    {
        if (element.LocalName != "canvas" || attribute is not ("width" or "height")) return;
        var width = CanvasDimension(element, "width", 300); var height = CanvasDimension(element, "height", 150); CheckCanvasBudget(element, width, height);
        if (canvases.TryGetValue(element, out var state))
        {
            state.Surface.Resize(width, height); state.TranslateX = state.TranslateY = 0; state.ScaleX = state.ScaleY = 1; state.Saved.Clear();
            state.Context.Set("fillStyle", "#000000", state.Context); state.Context.Set("strokeStyle", "#000000", state.Context); state.Context.Set("font", "10px sans-serif", state.Context); state.Context.Set("globalAlpha", 1, state.Context);
            CanvasChanged(element, state);
        }
    }
    private void CheckCanvasBudget(IElement element, int width, int height)
    {
        if (width < 0 || height < 0 || width > 4096 || height > 4096 || (long)width * height > 1_048_576) throw new BrowserLimitException("Canvas dimensions exceed the supported pixel budget.");
        long pixels = (long)width * height; foreach (var pair in canvases) if (pair.Key != element) pixels += (long)pair.Value.Surface.Width * pair.Value.Surface.Height;
        if (pixels > Math.Min(4_194_304, limits.MaximumScriptMemoryBytes / 4) || !canvases.ContainsKey(element) && canvases.Count >= 32) throw new BrowserLimitException("Canvas backing stores exceed the page pixel budget.");
    }
    private static int CanvasDimension(IElement element, string name, int fallback) => int.TryParse(element.GetAttribute(name), NumberStyles.None, CultureInfo.InvariantCulture, out var value) && value >= 0 ? value : fallback;
    private JsValue ImageData(BrowserCanvasSurface? surface, int x, int y, int width, int height)
    {
        if (width <= 0 || height <= 0 || (long)width * height > 1_048_576) throw new BrowserLimitException("ImageData exceeds its pixel budget.");
        var data = CallInternal(bootstrap.Get("pixelArray"), [checked(width * height * 4)]).AsObject();
        for (var row = 0; row < height; ++row) for (var col = 0; col < width; ++col)
        {
            var pixel = surface?.Pixel(x + col, y + row) ?? 0; var index = (row * width + col) * 4;
            data.Set(index.ToString(CultureInfo.InvariantCulture), (int)(pixel >> 16 & 255), data); data.Set((index + 1).ToString(CultureInfo.InvariantCulture), (int)(pixel >> 8 & 255), data);
            data.Set((index + 2).ToString(CultureInfo.InvariantCulture), (int)(pixel & 255), data); data.Set((index + 3).ToString(CultureInfo.InvariantCulture), (int)(pixel >> 24), data);
        }
        var result = new BrowserHostObject(engine); Data(result, "data", data); Data(result, "width", width); Data(result, "height", height); return result;
    }
    private static void PutImageData(BrowserCanvasSurface surface, JsValue value, int x, int y)
    {
        if (!value.IsObject()) throw new ArgumentException("Expected ImageData."); var image = value.AsObject(); var width = NumberIndex(image.Get("width")); var height = NumberIndex(image.Get("height"));
        if ((long)width * height > 1_048_576 || !image.Get("data").IsObject()) throw new BrowserLimitException("ImageData exceeds its pixel budget.");
        var data = image.Get("data").AsObject();
        for (var row = 0; row < height; ++row) for (var col = 0; col < width; ++col)
        {
            var index = (row * width + col) * 4;
            uint r = (uint)Math.Clamp(Number(data.Get(index.ToString(CultureInfo.InvariantCulture))), 0, 255), g = (uint)Math.Clamp(Number(data.Get((index + 1).ToString(CultureInfo.InvariantCulture))), 0, 255);
            uint b = (uint)Math.Clamp(Number(data.Get((index + 2).ToString(CultureInfo.InvariantCulture))), 0, 255), a = (uint)Math.Clamp(Number(data.Get((index + 3).ToString(CultureInfo.InvariantCulture))), 0, 255);
            surface.PutPixel(x + col, y + row, a << 24 | r << 16 | g << 8 | b);
        }
        surface.Commit();
    }
    private static BrowserFont CanvasFont(JsValue font)
    {
        var text = font.IsString() ? font.AsString() : "10px sans-serif"; var match = Regex.Match(text, @"(?<size>\d+(?:\.\d+)?)px\s+(?<family>.+)", RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(50));
        return match.Success && double.TryParse(match.Groups["size"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var size) ? new(Math.Clamp(size, 1, 256), match.Groups["family"].Value, text.Contains("bold", StringComparison.Ordinal) ? 700 : 400, text.Contains("italic", StringComparison.Ordinal)) : new(10, "sans-serif");
    }
    private static double Number(JsValue value) => value.IsNumber() && double.IsFinite(value.AsNumber()) ? value.AsNumber() : 0;
    private static int NumberIndexSigned(JsValue value) => (int)Math.Clamp(Number(value), int.MinValue, int.MaxValue);
}