using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly Dictionary<string, BrowserCanvasSurface?> images = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> imageFormats = new(StringComparer.Ordinal);
private long imagePixels;
private bool executingPageScript;
private string ImageKey(string source)
{
if (source.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return source;
return ResourceAddress(source).AbsoluteUri;
}
private BrowserCanvasSurface? FindImage(string source)
{
if (source.Length == 0) return null;
try { return images.GetValueOrDefault(ImageKey(source)); }
catch (Exception e) when (e is UriFormatException or NotSupportedException) { return null; }
}
private bool LoadImage(string source)
{
if (source.Length == 0) return false;
string key;
try { key = ImageKey(source); }
catch (Exception error) when (error is UriFormatException or NotSupportedException)
{ Diagnostic("Image failed: " + error.Message); return false; }
if (images.ContainsKey(key)) return false;
if (images.Count >= activeLimits.MaximumExternalResources) return false;
images.Add(key, null); // Failed images are not repeatedly fetched on each frame.
try
{
if (scriptPolicy?.BlocksAllScripts == true) throw new NotSupportedException("The declared image policy cannot be enforced.");
CheckOperationCancellation();
byte[] bytes;
if (key.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
int comma = key.IndexOf(',');
if (comma < 0 || comma > 256 || !key.AsSpan(5, comma - 5).StartsWith("image/", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("The image data URL is invalid.");
if (key.Length - comma - 1 > activeLimits.MaximumImageBytes * 3L) throw new BrowserLimitException("The image data URL exceeds its byte limit.");
bytes = key[..comma].EndsWith(";base64", StringComparison.OrdinalIgnoreCase)
? Convert.FromBase64String(key[(comma + 1)..]) : System.Text.Encoding.UTF8.GetBytes(Uri.UnescapeDataString(key[(comma + 1)..]));
}
else
{
var response = resourceLoader!.Get(new Uri(key), "image");
bytes = response.BinaryBody ?? throw new NotSupportedException("The transport did not supply binary image bytes.");
}
if (bytes.Length > activeLimits.MaximumImageBytes) throw new BrowserLimitException("The image exceeds its byte limit.");
int remaining = (int)Math.Min(activeLimits.MaximumImagePixels, activeLimits.MaximumTotalImagePixels - imagePixels);
var surface = BrowserImageDecoder.Decode(bytes, remaining);
images[key] = surface; imagePixels += (long)surface.Width * surface.Height;
imageFormats[key] = BrowserPngDecoder.HasSignature(bytes) ? "PNG" : bytes.Length >= 12 && System.Text.Encoding.ASCII.GetString(bytes, 8, 4) == "WEBP" ? "WebP" :
bytes.Length >= 3 && bytes[0] == 255 && bytes[1] == 216 ? "JPEG" : bytes.Length >= 3 && System.Text.Encoding.ASCII.GetString(bytes, 0, 3) == "GIF" ? "GIF" : "SVG";
Trace?.Invoke("XFE_BROWSER_IMAGE|Address|" + (key.StartsWith("data:") ? "DataUrl" : key) + "|Width|" + surface.Width + "|Height|" + surface.Height + "|Bytes|" + bytes.Length + "\n");
InvalidateDocument(); return true;
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Image failed: " + error.Message); return false; }
}
private void LoadImage(IElement element)
{
string source = element.GetAttribute("src") ?? "";
LoadImage(source);
if (source.Length != 0) dom?.DispatchEvent(element, FindImage(source) is not null ? "load" : "error", new() { Bubbles = false, Cancelable = false });
}
private bool LoadDisplayImages(BrowserDisplayList list)
{
if (executingPageScript || runtime?.IsExecutingScript == true || dom?.IsExecutingCallback == true) return false;
bool changed = false;
foreach (var command in list.Commands.Where(c => c.Kind == BrowserDrawKind.Image && c.Opacity > 0 &&
(c.Bounds.Width > 0 && c.Bounds.Height > 0 || c.Node is IElement { LocalName: "img" })).ToArray())
changed |= LoadImage(command.ImageSource);
return changed;
}
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly Dictionary<string, BrowserCanvasSurface?> images = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> imageFormats = new(StringComparer.Ordinal);
private long imagePixels;
private bool executingPageScript;
private string ImageKey(string source)
{
if (source.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return source;
return ResourceAddress(source).AbsoluteUri;
}
private BrowserCanvasSurface? FindImage(string source)
{
if (source.Length == 0) return null;
try { return images.GetValueOrDefault(ImageKey(source)); }
catch (Exception e) when (e is UriFormatException or NotSupportedException) { return null; }
}
private bool LoadImage(string source)
{
if (source.Length == 0) return false;
string key;
try { key = ImageKey(source); }
catch (Exception error) when (error is UriFormatException or NotSupportedException)
{ Diagnostic("Image failed: " + error.Message); return false; }
if (images.ContainsKey(key)) return false;
if (images.Count >= activeLimits.MaximumExternalResources) return false;
images.Add(key, null); // Failed images are not repeatedly fetched on each frame.
try
{
if (scriptPolicy?.BlocksAllScripts == true) throw new NotSupportedException("The declared image policy cannot be enforced.");
CheckOperationCancellation();
byte[] bytes;
if (key.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
int comma = key.IndexOf(',');
if (comma < 0 || comma > 256 || !key.AsSpan(5, comma - 5).StartsWith("image/", StringComparison.OrdinalIgnoreCase))
throw new NotSupportedException("The image data URL is invalid.");
if (key.Length - comma - 1 > activeLimits.MaximumImageBytes * 3L) throw new BrowserLimitException("The image data URL exceeds its byte limit.");
bytes = key[..comma].EndsWith(";base64", StringComparison.OrdinalIgnoreCase)
? Convert.FromBase64String(key[(comma + 1)..]) : System.Text.Encoding.UTF8.GetBytes(Uri.UnescapeDataString(key[(comma + 1)..]));
}
else
{
var response = resourceLoader!.Get(new Uri(key), "image");
bytes = response.BinaryBody ?? throw new NotSupportedException("The transport did not supply binary image bytes.");
}
if (bytes.Length > activeLimits.MaximumImageBytes) throw new BrowserLimitException("The image exceeds its byte limit.");
int remaining = (int)Math.Min(activeLimits.MaximumImagePixels, activeLimits.MaximumTotalImagePixels - imagePixels);
var surface = BrowserImageDecoder.Decode(bytes, remaining);
images[key] = surface; imagePixels += (long)surface.Width * surface.Height;
imageFormats[key] = BrowserPngDecoder.HasSignature(bytes) ? "PNG" : bytes.Length >= 12 && System.Text.Encoding.ASCII.GetString(bytes, 8, 4) == "WEBP" ? "WebP" :
bytes.Length >= 3 && bytes[0] == 255 && bytes[1] == 216 ? "JPEG" : bytes.Length >= 3 && System.Text.Encoding.ASCII.GetString(bytes, 0, 3) == "GIF" ? "GIF" : "SVG";
Trace?.Invoke("XFE_BROWSER_IMAGE|Address|" + (key.StartsWith("data:") ? "DataUrl" : key) + "|Width|" + surface.Width + "|Height|" + surface.Height + "|Bytes|" + bytes.Length + "\n");
InvalidateDocument(); return true;
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Image failed: " + error.Message); return false; }
}
private void LoadImage(IElement element)
{
string source = element.GetAttribute("src") ?? "";
LoadImage(source);
if (source.Length != 0) dom?.DispatchEvent(element, FindImage(source) is not null ? "load" : "error", new() { Bubbles = false, Cancelable = false });
}
private bool LoadDisplayImages(BrowserDisplayList list)
{
if (executingPageScript || runtime?.IsExecutingScript == true || dom?.IsExecutingCallback == true) return false;
bool changed = false;
foreach (var command in list.Commands.Where(c => c.Kind == BrowserDrawKind.Image && c.Opacity > 0 &&
(c.Bounds.Width > 0 && c.Bounds.Height > 0 || c.Node is IElement { LocalName: "img" })).ToArray())
changed |= LoadImage(command.ImageSource);
return changed;
}
}