From 587201d3f61f538d573c40460fcb510e306f80db Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Fri, 23 Oct 2026 09:01:34 +0200 Subject: [PATCH 1/3] Prepare for next release --- AGENTS.md | 12 +- CHANGELOG.md | 15 + src/AngleSharp.Renderer.Docs/package.json | 2 +- .../BrowserIntegrationTests.cs | 325 +++++++++++++++++ .../FontFallbackTests.cs | 30 ++ .../HtmlRendererTests.cs | 10 +- .../web-safe-font-families.linux.png | Bin 9230 -> 9244 bytes .../HtmlRenderer.WritingModes.cs | 5 + src/AngleSharp.Renderer/HtmlRenderer.cs | 339 ++++++++++++++---- .../Rendering/ImageResourceService.cs | 89 +++++ .../RenderingConfigurationExtensions.cs | 11 +- .../Skia/SkiaRenderBackend.cs | 24 +- .../Skia/SkiaTextMeasurer.cs | 2 +- .../Skia/SkiaTextShaping.cs | 225 +++++++++++- src/Directory.Build.props | 2 +- 15 files changed, 984 insertions(+), 107 deletions(-) create mode 100644 src/AngleSharp.Renderer.Tests/BrowserIntegrationTests.cs create mode 100644 src/AngleSharp.Renderer/Rendering/ImageResourceService.cs diff --git a/AGENTS.md b/AGENTS.md index d5174f1..e68817f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,6 +37,8 @@ The renderer and test projects target `net8.0` and `net10.0`; both runtimes are 3. Installed families - host-dependent, never assert them in snapshots. 4. Bundled sans-serif as the last resort. +A character the resolved typeface has no glyph for falls back per character (`SplitFallbackRuns`, shared by measuring and painting): the next `font-family` entry that has it, then the bundled fonts, then `SKFontManager.MatchCharacter` (host-dependent, so never in snapshots). Text the typeface fully covers stays one run. + Never resolve named families with `SKTypeface.FromFamilyName`: it substitutes the host default for unknown names instead of returning null, swallowing the rest of the fallback list. Availability goes through a case-insensitive index of installed families (Skia's lookup is case-sensitive on Linux only). ## Upstream Policy (important) @@ -54,7 +56,10 @@ Upstream status: everything filed so far is fixed as of 1.2.0; no failing upstre - **An authored `height` is the height** of block and flex containers; in-flow content overflows it (only `min-height`/`max-height` clamp it). Auto heights come from the laid-out content. - **`box-sizing`** is converted in exactly one place, `ResolveAuthoredDimension`; everything downstream works in content-box sizes. Flex item sizes are already converted before `ResolveFlexibleContentDimension` reads them - do not convert twice. Tables honor it too (their width goes through the same method); the HTML default `table { box-sizing: border-box }` has to come from the UA sheet. - **Style maps.** `CreateStyleMap` builds a per-element `StyleMap` (string map plus a typed raw-length side channel used by `ParseLength`). Always go through `GetOrCreateStyleMap` (cached per render call in `LayoutContext.StyleMapCache`; the cache must never outlive one `BuildDisplayList`/`RenderToPng`/`CaptureLayoutMetrics` call or interactive state goes stale). Code writing directly into a length entry must call `StyleMap.ClearRawLength` for that key. -- **Explicit vs computed values.** `background-image`, `grid-template-*`, gaps and `grid-column`/`grid-row` are read from the cascaded-but-uncomputed declaration (`ResolveExplicitPropertyValue` via the cached `LayoutContext.StyleCollection`), because AngleSharp.Css's `.Compute()` resolves their percentages against the viewport instead of the element's box. +- **Explicit vs computed values.** `background-image`, `grid-template-*`, gaps and `grid-column`/`grid-row` are read from the cascaded-but-uncomputed declaration (`ResolveExplicitPropertyValue` via the cached `LayoutContext.StyleCollection`), because AngleSharp.Css's `.Compute()` resolves their percentages against the viewport instead of the element's box. `width`/`height`/`min-*`/`max-*` take the explicit value only when it contains a `%`; anything else (`em`, `rem`, `vw`) needs the computed one. +- **The page.** `` is laid out as an ordinary block (so ``'s UA `margin: 8px`, padding, border and width apply); `context.Padding` is 0. `PropagateToViewport` moves the root's background (or ``'s, if the root has none) to the canvas and removes it from that element's style map, and drops their `overflow`. ``'s metrics are re-recorded as the viewport, since it is the scrolling element. Structural tests in `HtmlRendererTests` run with a prepended `body { margin: 0 }` sheet (its `ParseAsync`). The vertical-page path still lays out ``'s children directly. +- **Blockification.** Floats and absolutely positioned boxes are block-level whatever their `display` (AngleSharp.Css does not blockify the computed value); `LayoutElement` must never hand them to an inline formatting context, which would lay them out again forever. +- **Margin collapsing** through first children is recursive (`TryGetFirstCollapsibleChildTopMargin`); a box whose margin its parent absorbed (`suppressNextBlockTopMargin`) absorbs the whole chain below it too. Floats and out-of-flow boxes are skipped; inline-level content (atomics included) stops the chain. - **Unset ≠ initial.** AngleSharp.Css often reports an unset property as an empty string rather than its initial value; parsers fall back to the inherited value (inherited properties) or the CSS initial value. - Adding a new `RenderPaint` (or other) subtype: check every place that pattern-matches on the existing ones (`PaintBackground` once silently dropped `RenderImagePaint`). - **Flow layout.** `LayoutFlowChildren` (used by `LayoutElement` and the page itself) splits children into block-level children and runs of inline-level content. A run of bare text keeps the paragraph path (`LayoutTextNode`/`LayoutWrappedText`/`WrapText`); any run containing an element goes through `LayoutInlineFormattingContext`: the subtree is flattened into `InlinePiece`s (whitespace collapsed across element boundaries, break opportunities per piece), greedily broken into line boxes, and each line is emitted with one baseline (strut + half-leading per piece + atomic margin boxes). Inline boxes paint per line fragment; block-level boxes inside inline content get a line of their own. @@ -88,6 +93,7 @@ Only the non-obvious parts; see XML doc remarks in `HtmlRenderer.cs` for detail. - Masks: `mask-image` layers (image/gradient) with `-size`/`-position`/`-repeat`/`-mode`/`-clip`/`-origin` (border box by default) become a `PushMaskCommand`/`PopMaskCommand` scope, innermost of the whole-element scopes; the backend composites the layers with `DstIn` (luminance via a luma color filter). `mask-composite` other than `add`, and `url(#svg-mask)` references, are not supported. - `backdrop-filter` is a `DrawBackdropFilterCommand` emitted right inside the transform scope. SkiaSharp 2.x has no backdrop save-layer, so the backend snapshots the surface behind the box, filters it and draws it back clipped to the rounded border box; content inside a still-open ancestor layer (opacity, filter, mask) is not part of that backdrop. - `translate`/`rotate`/`scale` compose before `transform` (`ToTranslateFunction` etc.), around the same `transform-origin`. +- `` data: AngleSharp disposes a response right after processing it, so ``'s `CurrentDownload` content is closed by paint time. `WithRendering()` registers `ImageResourceService` (`IResourceService`), which buffers the bytes per response; `TryLoadImageResource` reads them from there. - Backgrounds: colors, `url(...)` images (`data:` or via `IDocumentLoader`, synchronous, shared per-document `DocumentImageCache` with ``; failure falls back to `background-color`), `background-repeat`/`-position`/`-size` resolved at paint time in `CreateImagePaint`. `no-repeat` uses `SKShaderTileMode.Decal`, not `Clamp`. `space`/`round` act as `repeat`. - Gradients: linear/radial/conic and `repeating-*` via `GradientParser`. Gotchas: Skia shader matrices map local → world space; `CreateSweepGradient` angles must stay within `[0, 360]` (rotation goes in the matrix); linear angles are shifted by -90° to Skia's convention in `ConvertLinearGradient`, conic does its own conversion; absolute-length stop positions stay unresolved (`AbsolutePositionPixels`) until the geometry is known. SVG gradients are a separate implementation. - `aspect-ratio` derives width from a definite height; `object-fit`/`object-position` apply to replaced elements. @@ -126,7 +132,7 @@ Every text feature exists twice - in the paragraph path (whole lines as strings) ### Form controls - A form control's look (border, padding, background, margins, round radio) comes from the UA stylesheet. `ApplyFormControlDefaults` only fills in the element-derived intrinsic size (text fields 150px, checkbox/radio from font size, buttons/selects/file inputs from their label) when `width`/`height` are unset. -- `PaintFormControl` paints values, labels, the selected option, checked fills, color swatches, range track/thumb and file inputs; `