Skip to content

Improve SVM to SVG conversion #772

Description

@andiwand

src/odr/internal/svm/svm_to_svg.cpp is a ~270 line best-effort translator that covers a small slice of the metafile action set. It is the only thing standing between an embedded StarView metafile (charts, OLE replacement images in ODF/OOXML) and the HTML output — html::write_image_src calls it, and on any exception silently falls back to emitting the raw .svm bytes as a data URL, which no browser renders. So every gap here shows up as a blank or wrong image, never as an error.

This issue is the umbrella for making that converter good. #194 (bitmaps) and #95 (font attributes) are sub-parts of it.

Resources — what actually exists

Answering the obvious first question: there is no formal specification. But we are not limited to reading LibreOffice from scratch either.

1. ONLYOFFICE's reverse-engineered spec — the best starting point

  • DesktopEditor/raster/Metafile/StarView/SPEC

    A prose specification modelled on Microsoft's WMF/EMF spec documents: signature, header, primitive types, basic/graphics objects, and the actions split into control / state-changing / drawing. Self-described provenance: "As far as we know, there is no formal specification of this format. This documentation has been created by reading the source code of LibreOffice." It is incomplete — several FIXMEs, an "Unsorted Action Types" section, Color unfinished, and polygon flag handling explicitly called out as unfinished — but it covers considerably more than we implement today, and it is far cheaper to read than the LibreOffice sources.

  • Alongside it, an independent C++ reader/player we can cross-check against: SvmEnums.h, SvmObjects.{h,cpp}, SvmFile.{h,cpp}, SvmPlayer.{h,cpp} in the same directory.

2. LibreOffice — the ground truth, and it already contains a metafile→SVG writer

The single most valuable reference is not the reader but the writer: LibreOffice's SVG export filter walks a GDIMetaFile and emits SVG, i.e. exactly our problem, solved.

3. Other implementations, for sanity-checking only

  • andiwand/svm — our own earlier Java implementation.
  • SoftarexTechnologies/svmconv — MIT, JS, renders to bitmap-on-canvas rather than SVG, ~11 commits, dormant. Of limited use, and it has a telling hack (it rewrites black to white because it ignores transparency).

There is nothing else: the remaining search hits are file-extension directory sites and online converters that just shell out to LibreOffice.

Where we stand

Handled today — drawing: RECT, POLYLINE, POLYGON, POLYPOLYGON, TEXT, TEXTARRAY, STRETCHTEXT. State: FILLCOLOR, LINECOLOR, OVERLINECOLOR, TEXTCOLOR, TEXTFILLCOLOR, FONT, TEXTLINE, MAPMODE.

Every other action falls into default: and is skipped. Against the 52 that svgwriter.cxx handles, the notable misses are:

Group Missing
Graphics state PUSH, POP, TEXTALIGN, RASTEROP, REFPOINT, TEXTLINECOLOR, LAYOUTMODE
Clipping CLIPREGION, ISECTRECTCLIPREGION, ISECTREGIONCLIPREGION, MOVECLIPREGION
Primitives PIXEL, POINT, LINE, ROUNDRECT, ELLIPSE, ARC, PIE, CHORD
Bitmaps (#194) BMP, BMPSCALE, BMPSCALEPART, BMPEX, BMPEXSCALE, BMPEXSCALEPART, MASK*
Fills GRADIENT, GRADIENTEX, HATCH, WALLPAPER
Transparency TRANSPARENT, FLOATTRANSPARENT
Text TEXTRECT (parsed by read_text_rectangle_action, never called)
Other EPS, COMMENT

Defects in what we already emit

Worth fixing before, or alongside, adding actions — these silently corrupt output we currently believe works:

  1. Text is not XML-escaped. write_text does out << text straight into the SVG. Any &, < or > in a chart label produces a malformed document; the browser then renders nothing. html::escape_text already exists in internal/html/common.hpp. This is the cheapest high-impact fix here.
  2. PUSH/POP are ignored, so the graphics state stack does not exist and colours/fonts set inside a push leak out of it and contaminate everything after. This is a whole-image correctness bug, not a missing feature.
  3. POLYPOLYGON holes fill solid. We emit one <polygon> per sub-polygon; the format means them as one path with a fill rule, so donuts, rings and letter counters come out filled. Should be a single <path> with fill-rule.
  4. Bézier flags are dropped. read_poly_line_action reads the has_flags byte and then // TODO flags not implemented; read_polygon has no flag concept at all. Every curve is currently rendered as a polyline through its control points, which is visibly wrong, not just approximate. (Acknowledged as hard — the ONLYOFFICE SPEC also flags it as unfinished.)
  5. Font size is not transformed. Coordinates go through transform_x/transform_y, but write_text_style emits font-size: context.font.size.y raw, so text scales independently of the drawing whenever the map mode scale is not 1:1.
  6. MapMode::unit is ignored. We use only origin and scale, so 100th-mm / twip / pixel units are all treated alike.
  7. TEXTALIGN missing ⇒ everything is anchored at the baseline-left default; centre- and right-aligned labels (i.e. most chart labels) sit in the wrong place.
  8. LineInfo is discarded. read_line_info fills width, dash/dot pattern and join, write_line_style uses none of it and hardcodes vector-effect:non-scaling-stroke.
  9. write_style(out, context, int) dispatches on magic 0/1/2 with a // TODO log or throw default. Wants an enum.
  10. Failures are invisible. Every unhandled action and every skipped byte carries a // TODO log; nothing is wired to Logger, and write_image_src swallows the exception path entirely. We cannot tell a file we rendered correctly from one we rendered as a blank rectangle.

Suggested order

  1. Escaping (1), state stack (2), poly-polygon fill rule (3), logging (10) — small, self-contained, fix output we already produce.
  2. Text correctness: TEXTALIGN, font-size transform, then the svm support #95 font attribute checklist (italic/bold/underline/strikeout).
  3. The missing primitives (LINE, ELLIPSE, ROUNDRECT, ARC, PIE, CHORD, POINT, PIXEL) — mechanical, one svgwriter.cxx case each.
  4. Clipping, then gradients/hatch, then transparency.
  5. Bitmaps (svm support bitmaps #194) — the largest single chunk, needs a DIB decoder.
  6. Béziers (4), and old version-1 SVM via SvmConverter.cxx, as stretch goals.

Test material already in tree: test/data/input/odr-public/svm/{chart-1,table-1}.svm, odr-private/svm/{test,Vyplaty}.svm, plus the odt/ods files named *svm*. Test coverage today is two smoke tests (svm_test.cpp) that only assert the output is non-empty — a real fix should come with reference-output comparison, and LibreOffice's own --convert-to svg makes a usable oracle.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions