Skip to content

feat(svm): fill with the gradients, hatches and transparency a metafile asks for - #789

Merged
andiwand merged 1 commit into
mainfrom
feat/svm-fills
Aug 30, 2026
Merged

feat(svm): fill with the gradients, hatches and transparency a metafile asks for#789
andiwand merged 1 commit into
mainfrom
feat/svm-fills

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stage 7 of #772, stacked on #788#787#786#785#784#779
review those first; this branch's base is feat/svm-primitives.

GRADIENT, GRADIENTEX, HATCH and TRANSPARENT were skipped. All four map
onto svg declaratively — a <linearGradient> or <radialGradient>, a
<pattern> of lines, an opacity — so nothing is rasterised.

LibreOffice as the oracle, and what it caught

None of these actions occurs in the 1125 metafiles harvested from the
fixtures, so there was nothing to check against. A metafile was written by hand
instead, converted with soffice --convert-to svg, and the two exports
compared. That found three things reading the format alone did not:

1. A colour inside an object is not a colour. An action's own colour
(LINECOLOR, FILLCOLOR, …) is a plain uint32 that SvmReader::ReadColor
reads. A gradient's two ends and a hatch's lines are not:
GenericTypeSerializer::readColor reads a uint16 name id first, and only
the user id (0x8000) carries three 16-bit channels behind it — anything else
indexes a palette of 31 colours. Reading four bytes there desynchronises the
stream, and the action-length check then throws away the whole image.
LibreOffice's export of the hand-written file said the gradient was black,
which is what a wrongly written colour looks like from the other side; the
same mistake reading would have cost every metafile with a gradient in it.

2. An axial ramp has its colours the other way round.
DrawLinearGradient swaps them (vcl/source/outdev/gradient.cxx): the end
colour is at both ends of the axis and the start colour in the middle.
LibreOffice's own export agrees — stop 0 = blue, stop 0.5 = red for a
red→blue axial gradient.

3. A radial ramp ends in the middle. DrawComplexGradient fills with the
start colour and shrinks rings inwards to the end colour, so in svg terms the
centre stop is the end colour.

The geometry

The linear ramp's vector is Gradient::GetBoundRect's: the bounds grown so
that turning still covers them, from the top of that to the bottom, turned
about the centre — (sin, cos) of the angle, because Polygon::Rotate turns
counter-clockwise. For the same action LibreOffice writes (450,100)→(750,400),
and so do we.

SQUARE and RECT shrink a rectangle rather than an ellipse, which svg has no
gradient for; they come out as the ellipse they are closest to, and that is
written down in svm/PLAN.md.

A hatch becomes a tiling <pattern> of one, two or three line sets, turned
by patternTransform — LibreOffice instead lays every line out individually
inside a pattern the size of the shape, which does not scale.

Verification

6 new tests (40 in the svm suite), asserting the gradient vectors and stop
order against the numbers LibreOffice produced. The reference output does not
move at all — none of these actions is in the corpus — so there is no
regeneration and no pin bump in this branch.

…le asks for

`GRADIENT`, `GRADIENTEX`, `HATCH` and `TRANSPARENT` were skipped. All four map
onto svg declaratively - a `<linearGradient>` or `<radialGradient>`, a
`<pattern>` of lines, an `opacity` - so nothing is rasterised.

None of them occurs in the 1125 metafiles harvested from the fixtures, so
LibreOffice was the oracle: a metafile written by hand for the purpose,
converted with `--convert-to svg`, and the two exports compared. That found
three things a reading of the format alone did not:

**A colour inside an object is not a colour.** An action's own colour is a
plain `uint32` that `SvmReader::ReadColor` reads; a gradient's two ends and a
hatch's lines are not. `GenericTypeSerializer::readColor` reads a `uint16` name
id first, and only the user id (`0x8000`) carries three 16-bit channels behind
it - anything else indexes a palette of 31. Reading four bytes there
desynchronises the stream, and the action-length check then throws the whole
image away. LibreOffice's export of the hand-written file said the gradient was
black, which is what a wrongly written colour looks like from the other side.

**An axial ramp has its colours the other way round.** `DrawLinearGradient`
swaps them: the end colour is at both ends of the axis, the start colour in the
middle.

**A radial ramp ends in the middle.** `DrawComplexGradient` fills with the
start colour and shrinks rings inwards towards the end colour.

The linear ramp's vector is `Gradient::GetBoundRect`'s: the bounds grown so
that turning still covers them, from the top of that to the bottom, turned
about the centre. For the same action LibreOffice writes (450,100) to
(750,400), and so do we.

`SQUARE` and `RECT` shrink a rectangle rather than an ellipse, which svg has no
gradient for; they come out as the ellipse they are closest to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmCr22NW6wPQKiQidk96bq
@andiwand
andiwand merged commit 9dcca2f into main Aug 30, 2026
29 checks passed
@andiwand
andiwand deleted the feat/svm-fills branch August 30, 2026 18:23
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