Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,27 @@ The release run heads these entries with the version and opens a fresh
- A spreadsheet decodes in less memory: 626 MB peak instead of 914 MB on a
297 MB `content.xml`. Rendered output is unchanged.

- A StarView metafile draws through a graphics state stack, so a colour, font
or map mode set inside a `PUSH` no longer leaks out of it and contaminates
the rest of the drawing. Nearly every metafile a document carries uses one.
- A text action in a StarView metafile draws the run it names, not the whole
string. Text drawn in runs used to overprint itself into a smear.

- A filled shape in a StarView metafile keeps its outline, its poly-polygons
cut their holes out, a line takes the width, dashing and join it carries, and
the font size scales with the drawing.
- StarView metafile text keeps its font: italic, bold, underline, strikeout,
rotation, alignment, per-character positions, and the width a stretched run
fills (#95).

- Text in a StarView metafile is escaped into the svg it renders as. An `&`,
`<` or `>` in a label made the svg malformed, and a malformed svg renders as
nothing.
- A StarView metafile draws through a graphics state stack: a colour, font or
map mode set inside a `PUSH` no longer leaks past its `POP`.

- A StarView metafile translation logs what it drops: the actions it does not
implement, and a translation failure it used to fall back from silently.
- A filled StarView shape keeps its outline, a poly-polygon cuts its holes, a
line takes its `LineInfo`, and the font size scales with the drawing.

- An html attribute value drops the control characters xml forbids, rather
than carrying them through. One escaper writes both html and svg now.
- Text in a StarView metafile is escaped into the svg it renders as. An `&` or
`<` in a label used to cost the whole image.

- A StarView metafile translation logs what it drops: unimplemented actions,
and a failure it used to fall back from silently.

- An html attribute value drops the control characters xml forbids. One
escaper writes both html and svg now.

## v6.12.0 - 2026-08-30

Expand Down
30 changes: 26 additions & 4 deletions src/odr/internal/svm/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ Each stage is one pull request, stacked on the one before it.
the fill that killed the stroke, the poly-polygon fill rule, the font size
in the transform, `LineInfo`. #772 defects 2, 3, 5, 8.
3. **Text.** `TEXTALIGN`, the `TEXTARRAY` dx array, the `STRETCHTEXT` width,
`TEXTRECT`, the #95 font attributes (bold, italic, underline, strikeout,
family), and decoding a non-`UCS2` string instead of passing its bytes
through β€” until then a latin-1 label emits invalid utf-8, which costs the
image exactly as an unescaped `&` did.
the run a text action names, and the #95 font attributes (bold, italic,
underline, strikeout, rotation). `TEXTRECT` is still open - it occurs
nowhere in the corpus - and so is decoding a non-`UCS2` string instead of
passing its bytes through: until then a latin-1 label emits invalid utf-8,
which costs the image exactly as an unescaped `&` did.
4. **Clipping.** `CLIPREGION`, `ISECTRECTCLIPREGION`,
`ISECTREGIONCLIPREGION`, `MOVECLIPREGION`.
5. **Bitmaps** (#194). See the shortcut below.
Expand Down Expand Up @@ -53,6 +54,9 @@ metafiles harvested from the `odt`/`ods` fixtures:
| `LINE` | 47 | 2 |
| `BMPEXSCALE` | 1 | 1 |

Half the text in the corpus is italic, all of it a formula variable, which is
what puts the #95 font attributes in stage 3 rather than later.

`ELLIPSE`, `ARC`, `PIE`, `CHORD`, `ROUNDRECT`, `POINT`, `PIXEL`, `GRADIENT`,
`HATCH`, `TRANSPARENT` and `EPS` do not occur at all, which is why they come
after clipping rather than before it. The one bitmap is the whole data area of
Expand All @@ -73,6 +77,24 @@ It is rare β€” 4 `MAPMODE` actions in 1125 files, one of them relative β€” and
getting it right means following `vcl/source/outdev/map.cxx` rather than
guessing, so it is its own stage.

## Text is where the corpus lives

Two thirds of every action in the corpus is text, and three things about it
are worth writing down:

- **`TextAlign` is vertical only.** It says whether the draw point is the top,
the baseline or the bottom of the run; vcl has no horizontal text alignment,
a run always starts at the point. Its default is `ALIGN_TOP`, which is not
what an svg `<text>` does, so it has to be written out. `svgwriter.cxx`
shifts the point by the font's ascent because it has the metrics; we name
`dominant-baseline` and let the browser do it.
- **A text action names a run**, `(index, length)`, of the string it carries -
and the string is the whole paragraph. Drawing the string rather than the run
overprints the sentence at every run's position.
- **The dx array and the stretch width are the file's own measurements**, and
they are what keeps a formula together when the viewer's font is not the
author's. They map onto an `x` list and `textLength` respectively.

## Shortcuts worth taking

- **Bitmaps are `.bmp` files already.** `SvmReader` reads them with
Expand Down
69 changes: 61 additions & 8 deletions src/odr/internal/svm/svm_format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,39 @@ std::string read_bytes(std::istream &in, const std::uint64_t size) {
}
}

/// `DrawText(…, index, len)`: a text action names the run of its string that
/// it draws, and a length past the end means the rest of it.
template <typename String>
String select_run(const String &text, const std::uint16_t offset,
const std::uint16_t length) {
if (offset >= text.size()) {
return {};
}
return text.substr(offset, length);
}

/// @ref select_run in the units the run is measured in - utf-16 code units for
/// `UCS2`, bytes otherwise. @p text has already been decoded to utf-8, where a
/// `UCS2` offset addresses nothing and a `substr` splits a character.
std::string select_run_with_encoding(const std::string &text,
const svm::TextEncoding encoding,
const std::uint16_t offset,
const std::uint16_t length) {
if (encoding != svm::RTL_TEXTENCODING_UCS2) {
return select_run(text, offset, length);
}
return util::string::u16string_to_string(
select_run(util::string::string_to_u16string(text), offset, length));
}

std::u16string read_u16string(std::istream &in, const std::uint32_t length) {
const std::string bytes =
read_bytes(in, static_cast<std::uint64_t>(length) * 2);
std::u16string result(length, u' ');
std::memcpy(result.data(), bytes.data(), bytes.size());
return result;
}

} // namespace

std::string svm::read_ascii_string(std::istream &in,
Expand All @@ -31,11 +64,7 @@ std::string svm::read_ascii_string(std::istream &in,

std::string svm::read_utf16_string(std::istream &in,
const std::uint32_t length) {
const std::string bytes =
read_bytes(in, static_cast<std::uint64_t>(length) * 2);
std::u16string result_u16(length, u' ');
std::memcpy(result_u16.data(), bytes.data(), bytes.size());
return util::string::u16string_to_string(result_u16);
return util::string::u16string_to_string(read_u16string(in, length));
}

std::string svm::read_uint16_prefixed_ascii_string(std::istream &in) {
Expand All @@ -56,6 +85,12 @@ std::string svm::read_uint16_prefixed_utf16_string(std::istream &in) {
return read_utf16_string(in, length);
}

std::u16string svm::read_uint16_prefixed_u16string(std::istream &in) {
std::uint16_t length;
read_primitive(in, length);
return read_u16string(in, length);
}

std::string svm::read_string_with_encoding(std::istream &in,
const TextEncoding encoding) {
if (encoding == RTL_TEXTENCODING_UCS2) {
Expand Down Expand Up @@ -426,7 +461,11 @@ svm::TextAction svm::read_text_action(std::istream &in, const VersionLength &vl,
read_primitive(in, result.length);

if (vl.version >= 2) {
result.text = read_uint16_prefixed_utf16_string(in);
result.text = util::string::u16string_to_string(select_run(
read_uint16_prefixed_u16string(in), result.offset, result.length));
} else {
result.text = select_run_with_encoding(result.text, encoding, result.offset,
result.length);
}

return result;
Expand All @@ -452,7 +491,11 @@ svm::TextArrayAction svm::read_text_array_action(std::istream &in,
}

if (vl.version >= 2) {
result.text = read_uint16_prefixed_utf16_string(in);
result.text = util::string::u16string_to_string(select_run(
read_uint16_prefixed_u16string(in), result.offset, result.length));
} else {
result.text = select_run_with_encoding(result.text, encoding, result.offset,
result.length);
}

return result;
Expand All @@ -470,7 +513,11 @@ svm::read_stretch_text_action(std::istream &in, const VersionLength &vl,
read_primitive(in, result.length);

if (vl.version >= 2) {
result.text = read_uint16_prefixed_utf16_string(in);
result.text = util::string::u16string_to_string(select_run(
read_uint16_prefixed_u16string(in), result.offset, result.length));
} else {
result.text = select_run_with_encoding(result.text, encoding, result.offset,
result.length);
}

return result;
Expand All @@ -492,6 +539,12 @@ svm::read_text_rectangle_action(std::istream &in, const VersionLength &vl,
return result;
}

std::uint16_t svm::read_text_align_action(std::istream &in) {
std::uint16_t result;
read_primitive(in, result);
return result;
}

std::uint16_t svm::read_push_action(std::istream &in, const VersionLength &vl) {
if (vl.length < sizeof(std::uint16_t)) {
return PUSH_ALL;
Expand Down
44 changes: 44 additions & 0 deletions src/odr/internal/svm/svm_format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,47 @@ enum TextEncoding {
RTL_TEXTENCODING_UCS2 = 0xFFFF,
};

/// `TextAlign`: which edge of the text the draw point names. Vertical only -
/// vcl has no horizontal text alignment, a run always starts at the point.
enum MetaTextAlign {
ALIGN_TOP = 0,
ALIGN_BASELINE = 1,
ALIGN_BOTTOM = 2,
};

/// `FontWeight`.
enum MetaFontWeight {
WEIGHT_DONTKNOW = 0,
WEIGHT_THIN = 1,
WEIGHT_ULTRALIGHT = 2,
WEIGHT_LIGHT = 3,
WEIGHT_SEMILIGHT = 4,
WEIGHT_NORMAL = 5,
WEIGHT_MEDIUM = 6,
WEIGHT_SEMIBOLD = 7,
WEIGHT_BOLD = 8,
WEIGHT_ULTRABOLD = 9,
WEIGHT_BLACK = 10,
};

/// `FontItalic`.
enum MetaFontItalic {
ITALIC_NONE = 0,
ITALIC_OBLIQUE = 1,
ITALIC_NORMAL = 2,
ITALIC_DONTKNOW = 3,
};

/// `FontLineStyle`, the underline; anything but `NONE` underlines.
enum MetaFontLineStyle {
LINESTYLE_NONE = 0,
};

/// `FontStrikeout`; anything but `NONE` strikes through.
enum MetaFontStrikeout {
STRIKEOUT_NONE = 0,
};

/// `LineStyle`, what a `LineInfo` draws with.
enum MetaLineStyle {
LINE_NONE = 0,
Expand Down Expand Up @@ -250,6 +291,7 @@ std::string read_utf16_string(std::istream &in, std::uint32_t length);
std::string read_uint16_prefixed_ascii_string(std::istream &in);
std::string read_uint32_prefixed_utf16_string(std::istream &in);
std::string read_uint16_prefixed_utf16_string(std::istream &in);
std::u16string read_uint16_prefixed_u16string(std::istream &in);
std::string read_string_with_encoding(std::istream &in, TextEncoding encoding);

VersionLength read_version_length(std::istream &in);
Expand Down Expand Up @@ -281,5 +323,7 @@ TextRectangleAction read_text_rectangle_action(std::istream &in,
TextLineAction read_text_line_action(std::istream &in, const VersionLength &vl);
/// The `PushFlags` of a `PUSH`. A version that carries none saves everything.
std::uint16_t read_push_action(std::istream &in, const VersionLength &vl);
/// The `TextAlign` of a `TEXTALIGN`.
std::uint16_t read_text_align_action(std::istream &in);

} // namespace odr::internal::svm
Loading
Loading