feat(svm): write the svg through a writer that escapes, and log what we drop - #779
Merged
Conversation
andiwand
force-pushed
the
feat/svm-svg-writer
branch
2 times, most recently
from
August 30, 2026 07:49
aac6e28 to
f716823
Compare
This was referenced Aug 30, 2026
andiwand
force-pushed
the
feat/svm-svg-writer
branch
from
August 30, 2026 08:37
a471320 to
b56b2bb
Compare
…we drop The svm to svg translator wrote markup straight into the stream, which means text went in unescaped: an `&`, `<` or `>` in a chart label makes the svg malformed, and a malformed svg renders as nothing at all - the whole image, not the one label. It also had no logger, so an action we do not implement, and the failure that falls back to emitting the raw `.svm` bytes as a data url no browser renders, were both silent. `svg::SvgWriter` is the counterpart to `html::HtmlWriter`: elements, attributes, style declarations and text, escaped, plus number formatting that does not depend on the stream's locale or precision. `svm_to_svg.cpp` writes through it, `Translator::svg` becomes `svm::translate_to_svg` and takes a `Logger`. Threading that logger down needed one: `abstract::HtmlService` gained `logger()`, so views reach the sink the service was built with, and `WritingState` carries it to `translate_image_src`. `write_image_src` now asks the file type before trying an svm translation rather than attempting one on every image and catching, which is what makes a failure worth logging. Tests build their metafile as bytes inline, so an action is testable without a fixture. Output on the four svm fixtures is unchanged except for coordinate precision, which improves: `std::to_chars` with three decimals where the stream's six significant digits used to round 10519.375 to 10519.4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmCr22NW6wPQKiQidk96bq
`svg::format_number` was a second implementation of `util::number::to_string_significant`, whose own doc comment already says "never in scientific notation, which CSS and SVG lengths do not accept". The two agree on every value the svg tests cover; they parted only below `1e-4`, where the stream-based one collapsed `1e-07` to `0`. The classic locale the svg writer wanted moves into `number_util`, so `Measure::to_string` gets it too. `svg::escape_text` / `escape_attribute` and `html::escape_attribute` were the same escape written twice - the html one as four `replace_all` passes over a by-value string. Both are now `util::xml::escape_text` / `escape_attribute`, one pass, dropping the control characters xml 1.0 cannot carry. `html::escape_text` stays in `html`: its ` ` and ` ` are presentation, not escaping. Also stops a `;` in a style value from opening a declaration of its own. It is dropped before escaping, which writes `;` of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
The catch narrowed to `std::exception` to report `what()`, which let anything else past `translate_image_src(const ImageFile &)`, where nothing catches it. A second catch keeps the old guarantee and still says the image is gone. Also widens the skip length explicitly, as its sibling call already did, and trims two comments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
`README.md` comes back rather than being replaced by `AGENTS.md` and `PLAN.md`: it is the checklist of what the translator does and does not draw, now checked against the code and extended with the shapes, fills, clipping and state handling that were never listed, plus the references completed with `SvmReader.cxx`, `SvmConverter.cxx`, `svgwriter.cxx` and the ONLYOFFICE spec. It also records the half of #772's escaping defect that escaping does not fix: `read_string_with_encoding` returns the file's own bytes for every encoding but `UCS2`, so a latin-1 label emits invalid utf-8 and costs the image exactly as an unescaped `&` did. Stage 3 in `PLAN.md` picks it up. The comments this branch added are cut back to the house standard - no restatement of the code, no narrative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XQpLmJpJ87qbKoG8B7kbLY
andiwand
force-pushed
the
feat/svm-svg-writer
branch
from
August 30, 2026 08:58
b56b2bb to
bd30986
Compare
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Stage 1 of #772 — the infrastructure the rest of the work stands on. The
staged plan is written down in
src/odr/internal/svm/PLAN.md; this PR is thebase of the stack.
The problem
svm_to_svg.cppwrote its markup straight into the stream. Two consequences:write_textdidout << text. An&,<or>in a chart label makes the svg malformed, and a malformed svg renders as
nothing — the browser drops the whole image, not the one label. (Improve SVM to SVG conversion #772
defect 1.)
// TODO log, andwrite_image_srccaught every exception and fell back to emitting the raw.svmbytes as a data url that no browser renders. A file we drew correctlyand a file we drew as a blank rectangle looked exactly alike. (Improve SVM to SVG conversion #772 defect
10.)
What is here
svg::SvgWriter(internal/svg/svg_writer.*), the counterpart tohtml::HtmlWriter: elements, attributes, style declarations and text, allescaped, with the
styleattribute accumulated per element so attributes andstyle can be written in any order. A
;in a style value is dropped ratherthan allowed to open a declaration of its own.
in
util::xml::escape_text/escape_attribute— one pass, dropping thecontrol characters xml 1.0 cannot carry — and
html::escape_attribute, fourreplace_allpasses over a by-value string, is gone in favour of it.html::escape_textstays inhtml: its and arepresentation, not escaping, and
is undefined in xml. Numbers gothrough
util::number::to_string_significant, which already documented"never in scientific notation, which CSS and SVG lengths do not accept"; the
classic locale it needed (a german one writes
1,5into a coordinate) movedin there, so
Measure::to_stringgets it too.Translator::svgbecomessvm::translate_to_svg(file, out, logger)and reports every action it skipsby name (
action_type_nameinsvm_format). Getting a logger there neededone:
abstract::HtmlServicegainedlogger(), so a view reaches the sinkits service was built with, and
WritingStatecarries it down totranslate_image_src.write_image_srcasks the file type before trying an svm translation,instead of attempting one on every image and catching. That is what makes a
failure worth a warning rather than noise on every png.
write_style(out, context, int)with its magic0/1/2is an enum now(Improve SVM to SVG conversion #772 defect 9).
svm/README.mdkeeps the feature checklist, now checked againstthe code and completed — the shapes, fills, clipping and state handling that
were never listed, and the references it was missing.
svm/AGENTS.md(theformat, the conventions) and
svm/PLAN.md(the stages) are new; a row in theroot
AGENTS.md; a section insvg/AGENTS.mdfor the writer.Tests
svm_test.cppbuilds its input as bytes, inline (SvmBuilder), so anaction gets a test without a fixture — the harness the following stages need.
Six new cases: the empty file, a rectangle, text, text escaping, attribute
escaping, and that an unimplemented action is skipped by its own length so the
one after it still reads.
Output is unchanged
Rendered the four svm fixtures and diffed the decoded svg against
main's:odr-public/svm/chart-1.svmodr-public/svm/table-1.svmodr-private/svm/test.svmodr-private/svm/Vyplaty.svmThe formatter can only part from
operator<<where a value would print withan exponent —
|v| ≥ 1e6, or0 < |v| < 1e-4— and no coordinate in the fourfixtures is in that range. Where it does bite it is the fix:
1051938ratherthan
1.05194e+06, which is not a css length at all.The full suite passes (1285 tests),
HtmlOutputTestsincluded, so the sharedescaper changes no reference rendering either. Decoding all 1144 generated
svgs in the reference output shows none of them currently malformed, so the
escaping fix changes no rendering today — the bug is latent in the corpus, not
absent from the wild.
Known gap, recorded rather than fixed
Escaping is only half of #772 defect 1.
read_string_with_encodinghands backthe file's own bytes for every encoding but
UCS2, so a latin-1 label emitsinvalid utf-8 and an xml parser refuses the document just as hard as an
unescaped
&made it.README.mdrecords it and stage 3 ofPLAN.mdpicksit up.