Skip to content

fix(svm): draw through a graphics state stack, and stop the fill killing the stroke - #784

Merged
andiwand merged 4 commits into
mainfrom
fix/svm-graphics-state
Aug 30, 2026
Merged

fix(svm): draw through a graphics state stack, and stop the fill killing the stroke#784
andiwand merged 4 commits into
mainfrom
fix/svm-graphics-state

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stage 2 of #772, stacked on #779review that one first; this branch's
base is feat/svm-svg-writer. It fixes the defects in what we already draw,
before any new action is added.

The graphics state stack (#772 defect 2)

PUSH and POP fell through to default: like any unimplemented action, so
there was no state stack: a colour, font or map mode set inside a push leaked
out of it and contaminated the rest of the drawing.

This is the most common thing we get wrong. Harvesting every embedded metafile
from the odt/ods fixtures — 1125 of them — gives:

action occurrences files (of 1125)
PUSH / POP 22317 each 1124
TEXTALIGN 21534 1117
STRETCHTEXT 20335 1097
ISECTRECTCLIPREGION 1124 1123

The flags matter as much as the stack: 20919 of those 22317 pushes save five of
the fourteen state groups (LINECOLOR|FILLCOLOR|FONT|TEXTCOLOR|MAPMODE), only
1140 save everything. Restoring the whole state on a pop would therefore be
wrong more often than restoring nothing, so PushFlags is modelled and a pop
puts back exactly what its push asked for.

Four more, all in what we already drew

  • The fill killed the stroke. A rect or polygon wrote the line style and
    then the fill style, and the fill style ended in stroke:none — so a filled
    shape never had an outline. In vcl the pen outlines what the brush fills.
  • A poly-polygon is one shape (Improve SVM to SVG conversion #772 defect 3). It was one <polygon> per
    sub-polygon, so donuts, rings and letter counters came out solid. Now one
    <path> with fill-rule:evenodd — which is what vcl fills with, and what
    LibreOffice's own svg export puts on its root element.
  • LineInfo was read and dropped (Improve SVM to SVG conversion #772 defect 8). Width, dash pattern and
    join now reach the stroke; a default LineInfo still gets the hairline.
  • The font size did not scale (Improve SVM to SVG conversion #772 defect 5). Coordinates went through the
    map mode and font-size did not, so text kept its size while the drawing
    shrank around it.

svgwriter.cxx's GetPathString is the model for the path data — which also
puts the bézier stage within reach, since its C segments are the same
function.

What is not here

The map mode's unit (#772 defect 6) is deferred to its own stage, and
svm/PLAN.md says why: MetaMapModeAction::Execute calls SetMapMode, which
replaces the map mode except when the new unit is MapRelative, where scales
multiply and origins add. We do neither. It is 4 actions in 1125 files and
wants vcl/source/outdev/map.cxx read properly rather than guessed at.

Verification

7 new tests (15 in the svm suite now), each built from inline bytes: a shape
draws both colours, an unset colour draws nothing, a pop restores what its push
saved, a pop keeps what its push did not save, a poly-polygon is one path
with the fill rule, a polyline takes its LineInfo, and the font size scales.

Rendering: A/B'd the four svm fixtures and the changed pages of the corpus in
headless Chrome. The charts and the 1032 formulas of
odt/Vektoranalysis Zusammenfassung.odt come out equivalent; test.svm's data
series is now drawn at the width the file asks for instead of as a hairline,
matching what LibreOffice renders.

This changes the emitted html for 23 files, so the reference output has to be
regenerated and the pin in test/data.cmake advanced before CI's compare step
can pass.
All 23 are documents that carry an svm; the public reference set is
untouched.

andiwand and others added 3 commits August 30, 2026 11:12
…ing the stroke

`PUSH` and `POP` were skipped like any unimplemented action, so the state
stack did not exist: a colour, font or map mode set inside a push leaked out of
it and contaminated everything after. That is the most common thing we got
wrong - of 1125 metafiles harvested from the odt/ods fixtures, 1124 push, 22317
times in all. The flags matter too: 20919 of those pushes save five of the
fourteen state groups, so restoring everything on a pop would be wrong far
more often than restoring nothing.

Four more, all in what we already drew:

- **The fill killed the stroke.** A rectangle or polygon wrote the line style
  and then the fill style, and the fill style ended in `stroke:none`, so a
  filled shape never had an outline. The pen outlines what the brush fills.
- **A poly-polygon is one shape.** It was one `<polygon>` per sub-polygon, so
  a donut, a ring and the counter of an "o" came out solid. Now one `<path>`
  with `fill-rule:evenodd`, which is what vcl fills with - LibreOffice's own
  svg export puts that rule on its root element.
- **A line's `LineInfo` was read and dropped.** Width, dash pattern and join
  now reach the stroke; only a default `LineInfo` keeps the hairline.
- **The font size did not scale.** Coordinates went through the map mode and
  `font-size` did not, so text kept its size while the drawing around it
  shrank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmCr22NW6wPQKiQidk96bq
The 23 private and 6 public files this branch changes, regenerated.
No restatement and no narrative: the seven test doc comments each said what
their own test name says, and the source comments that told a story are cut to
the point they carry. The one fact worth keeping - that half the pushes in the
corpus save a subset of `PushFlags`, which is why a `POP` must restore only
what its `PUSH` named - moves to `AGENTS.md`, where rationale belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
`write_path({std::move(points)}, ...)` moved nothing: a braced-init-list is a
`std::initializer_list`, whose elements are `const`, so each call copied the
whole point vector. A span carries the one polygon where it already is, and
the poly-polygon case is unchanged.

Also records why a stroke width and a dash go through the x scale alone, which
reads like an oversight and is not: `svgwriter.cxx` maps them with
`ImplMap(sal_Int32)`, which builds a square and takes its `Width()`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
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