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);
}
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);
}