Skip to content

feat(svg): honour the opacity family and warn on what the reader approximates - #596

Merged
DemchaAV merged 1 commit into
developfrom
fix/svg-reader-hardening
Aug 24, 2026
Merged

feat(svg): honour the opacity family and warn on what the reader approximates#596
DemchaAV merged 1 commit into
developfrom
fix/svg-reader-hardening

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Why

The beta SVG reader read opacity, fill-opacity and stroke-opacity from nowhere: a translucent logo landed on the page fully opaque, an opacity="0" guide layer an exporter left behind painted at full strength, and the only fix was editing the SVG by hand. The reader's silence had the same shape elsewhere — fill-rule="evenodd" filled with non-zero winding without a word (a donut whose hole is cut by a same-direction subpath comes out solid), a mask/filter/pattern vanished with no signal, and the DOCX export dropped a paragraph's inline SVG icons while the block-level drop path warned. Three parse edges (#zzz hex, non-numeric rgb() channels, unit-only lengths) leaked the raw NumberFormatException past the reader's name-the-field-and-element error convention. This is the "hardens against real-world exporter output" work the surface has carried @Beta for since 1.8.0.

What changed

  • SvgIconReader honours the opacity family. fill-opacity / stroke-opacity ride the inherited Paint state and an explicit value replaces the inherited one (SVG property inheritance); opacity composites — each carrier multiplies into an accumulated group product, the standard per-layer approximation of SVG's offscreen group compositing. The product multiplies into each layer's flat paint alpha via SvgStyles.multiplyAlpha, on top of any alpha the colour already carries from 8-digit hex or rgba(). A slot whose product reaches zero is treated like fill="none". A partial opacity cannot reach a gradient slot — the DocumentPaint.Stop contract refuses translucent stops because shadings carry no alpha — so the gradient paints opaque and the degradation is warned once per icon.
  • SkipTally grew a degraded-features channel. fill-rule="evenodd" renders as before (non-zero) but warns once, noted at layer-emit time so the tally never names a layer that was dropped as degenerate. A mask="url(#id)" / filter="url(#id)" attribute warns even when the definition sits in <defs> (which the walk never visits) — the referencing attribute is the only place the divergence is observable. Element kinds outside the reader's vocabulary (mask, pattern, marker, <style> CSS, <a> wrappers…) join the skip tally that previously only counted text / image / use; a blank icon's "no drawable geometry" error names them. defs / title / desc / gradients / clipPath / symbol stay silent — they hold no direct ink.
  • SvgStyles keeps the error house style at the edges. Malformed hex digits, non-numeric rgb() channels / rgba() alpha, and unit-only lengths now throw contextual IllegalArgumentExceptions with the JDK cause chained; opacity="NaN" is refused rather than silently hiding the subtree (NaN fails every > 0 check). An out-of-vocabulary fill-rule value is refused like any other bad presentation value — previously the attribute was unread, so this is a new loud edge, recorded in the CHANGELOG.
  • DocxSemanticBackend.warnDroppedInlineRuns — the inline mirror of the existing block-level warnUnsupported: image / shape / SVG runs (emoji lower to SVG runs) vanish from a paragraph by contract and now say so once per export per kind, deduplicated through the same warnedNodeKinds set that resets in export().
  • Javadoc stopped describing a younger reader. SvgGradients claimed focal radials and stop-opacity are "loudly refused" — both degrade deliberately (centred-radial approximation, opaque stops, alpha-only overlays dropped). SvgIcon still listed clip paths as out of scope and the tally warning said "no clips" — clip-path:url(#id) has rendered since 1.9.0. DocumentPaint.Stop said the reader "refuses" stop-opacity. All three now state what actually happens.
  • Two committed previews refreshedemoji-clip-path.pdf and emoji-shortcodes.pdf are the only committed renders whose source SVGs carry element opacity (401 of the 1595 bundled emoji glyphs do); the visible delta is small (~100 px at 2× raster) because most glyph transparency rides gradients, which are unchanged. Template CV icons and the showcase art use only fill-rule="evenodd", which renders identically and merely warns — no other preview moves.

Verification

Full reactor gate ./mvnw -B -ntp clean verify -pl :graph-compose-core,…,:graph-compose-coverage -amBUILD SUCCESS, all coverage checks met, committed layout snapshots and visual baselines untouched. +31 tests: SvgIconOpacityAndWarningsTest (22 — opacity inheritance vs composition, style="" carriage, zero-hides-slot for fill/stroke/whole layer, hex-alpha multiplication, gradient fill/stroke opaque-plus-warn, NaN refusal, evenodd warn-once and quiet nonzero/inherit, mask-in-defs and style="filter:…" warns, unknown-element tally incl. the empty-icon error, structure-only silence), SvgStylesTest +5 (hex/rgb NFE wrapping, channel clamping, unit-only length, opacity grammar), SvgPathTest +2 (scientific-notation numbers, non-zero arc x-axis-rotation endpoint), DocxInlineRunDropWarningTest (2 — one warn per export naming InlineSvgRun with paragraph text preserved, text-only export stays quiet). Existing 40-case SvgIconTest and the qa render suites stay green. No public API signature change — behaviour and messages only.

Lane: canonical (document.svg) + render-docx warn seam — no engine/layout surface touched.

Notes

  • Group opacity multiplies down the tree, so overlapping siblings inside one translucent group darken where a browser would flatten the group first — the trade every lightweight icon reader makes; honouring it exactly needs offscreen compositing the fixed-layout pipeline doesn't have.
  • Full even-odd filling and translucent gradient stops both end at engine contracts (PathNode has no winding-rule slot; DocumentPaint.Stop rejects alpha) — each is a separate feature if ever wanted, not part of this hardening.

@DemchaAV
DemchaAV merged commit e6f6035 into develop Aug 24, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/svg-reader-hardening branch August 24, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant