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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ 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 clips what it says it clips: `CLIPREGION` and the two
that intersect one into it. A drawing that ran outside the clip used to be
shown in full.

- A StarView metafile draws its bitmaps (#194): `BMP`, `BMPEX` and their
scaling and part variants, transparency mask included. Such a chart used to
render as an empty frame.
Expand Down
23 changes: 21 additions & 2 deletions src/odr/internal/svm/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Each stage is one pull request, stacked on the one before it.
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`.
4. **Clipping** - done but for `MOVECLIPREGION`, which occurs nowhere and
would have to move path data that is already written out.
5. **Bitmaps** (#194) - done for the `BMP` and `BMPEX` families; `MASK`,
which stencils one colour through a bitmap, and `ZCOMPRESS`, which needs
inflating first, are still open. See the shortcut below.
Expand Down Expand Up @@ -79,6 +79,25 @@ 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.

## Clipping is nested groups

An svg `clip-path` names one shape, and `ISECT…` intersects. Two clips
therefore become two groups, one inside the other, and the state's clip is a
*stack* of shapes rather than one: `ensure_clip` keeps the groups that the
next drawing action still wants, closes the ones it does not, and opens what
is missing. A `POP` that restores the clip is then nothing special - the next
action closes what it has to.

A region streams as a band list, which is a union of rectangles, and from
version 2 also as the poly-polygon those were rasterised from. The polygons
are the better outline where they are there. Both go into one `<path>` -
disjoint bands make union and even-odd the same thing, and a poly-polygon
wants even-odd anyway, so `clip-rule="evenodd"` covers both.

vcl does *not* re-scale a clip when the map mode changes (`SetMapMode`:
"clip regions are not re-scaled"), so the shape is transformed once, when the
action sets it.

## Text is where the corpus lives

Two thirds of every action in the corpus is text, and three things about it
Expand Down
70 changes: 70 additions & 0 deletions src/odr/internal/svm/svm_format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <array>
#include <cstdint>
#include <cstring>
#include <optional>
#include <stdexcept>
#include <utility>

namespace odr::internal {

Expand All @@ -23,6 +25,14 @@ std::string read_bytes(std::istream &in, const std::uint64_t size) {
}
}

/// `RegionType`, what shape the region was streamed as.
constexpr std::uint16_t region_null = 0;
constexpr std::uint16_t region_empty = 1;
/// `StreamEntryType`, what the band list holds.
constexpr std::uint16_t band_header = 0;
constexpr std::uint16_t band_separation = 1;
constexpr std::uint16_t band_end = 2;

/// A `.bmp` starts with `"BM"`; `"BA"`, an os/2 bitmap array, does not.
constexpr std::uint16_t bmp_magic = 0x4d42;
constexpr std::uint32_t bmp_file_header_size = 14;
Expand Down Expand Up @@ -777,6 +787,66 @@ svm::BitmapAction svm::read_bitmap_action(std::istream &in,
return result;
}

std::optional<svm::Region> svm::read_region(std::istream &in) {
const VersionLength vl = read_version_length(in);
std::uint16_t content_version{};
std::uint16_t type{};
read_primitive(in, content_version);
read_primitive(in, type);

if (type == region_null) {
return std::nullopt;
}

Region result;
if (type == region_empty) {
return result;
}

// horizontal strips, each with the spans inside the region
std::int32_t top{};
std::int32_t bottom{};
while (true) {
std::uint16_t entry{};
read_primitive(in, entry);
if (entry == band_end) {
break;
}

std::int32_t first{};
std::int32_t second{};
read_primitive(in, first);
read_primitive(in, second);

if (entry == band_header) {
top = first;
bottom = second;
} else if (entry == band_separation) {
result.rectangles.push_back({first, top, second, bottom});
} else {
throw MalformedSvmFile();
}
}

if (vl.version >= 2) {
bool has_polygons{};
read_primitive(in, has_polygons);
if (has_polygons) {
result.polygons = read_poly_polygon(in);
}
}

return result;
}

std::pair<std::optional<svm::Region>, bool>
svm::read_clip_region_action(std::istream &in) {
std::optional<Region> region = read_region(in);
bool clip{};
read_primitive(in, clip);
return {std::move(region), clip};
}

std::uint16_t svm::read_text_align_action(std::istream &in) {
std::uint16_t result;
read_primitive(in, result);
Expand Down
17 changes: 17 additions & 0 deletions src/odr/internal/svm/svm_format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

#include <cstdint>
#include <istream>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

// https://github.com/LibreOffice/core/blob/master/include/vcl/metaact.hxx
Expand Down Expand Up @@ -267,6 +269,15 @@ struct TextRectangleAction final {
std::uint16_t style{};
};

/// A clip region: bands covering it as a union of rectangles, and from
/// version 2 the poly-polygon those were rasterised from. An *empty* region
/// covers nothing and so clips everything away; `REGION_NULL`, which does not
/// clip at all, is no region and reads as `std::nullopt`.
struct Region final {
std::vector<Rectangle> rectangles;
std::vector<std::vector<IntPair>> polygons;
};

/// A dib as something a browser reads. A metafile stores a dib *with* its
/// `BITMAPFILEHEADER`, so its bytes already are a `.bmp` file; they are only
/// unpacked where a png would be smaller, which for a chart is by fifty.
Expand Down Expand Up @@ -354,6 +365,12 @@ TextLineAction read_text_line_action(std::istream &in, const VersionLength &vl);
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);
/// A region, as `ReadRegion` reads one: a band list, and from version 2 the
/// poly-polygon it came from. `std::nullopt` where it does not clip at all.
std::optional<Region> read_region(std::istream &in);
/// A `CLIPREGION`: the region, and whether it clips at all.
std::pair<std::optional<Region>, bool>
read_clip_region_action(std::istream &in);
/// A dib with its file header, as `ReadDIB(…, bFileHeader=true)` reads one.
/// @p limit is what the enclosing action declared, so a length field cannot
/// ask for more than the file holds.
Expand Down
108 changes: 108 additions & 0 deletions src/odr/internal/svm/svm_to_svg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <algorithm>
#include <cmath>
#include <optional>
#include <ranges>
#include <span>
#include <string>
Expand Down Expand Up @@ -43,6 +44,9 @@ struct GraphicsState final {
std::uint32_t over_line_rgb{};
/// vcl's default, and what a file that never says otherwise draws with.
std::uint16_t text_align{ALIGN_TOP};
/// What the drawing is clipped to, as path data, one entry per region the
/// file intersected in. Nesting a group per entry is what intersects them.
std::vector<std::string> clip;
};

struct SavedState final {
Expand All @@ -61,6 +65,10 @@ struct Context final {
/// Names the masks and clip paths apart.
std::uint32_t element_count{};
bool inverter_written{};

/// The clip the open groups already apply - a prefix of the state's clip
/// once @ref ensure_clip has run.
std::vector<std::string> written_clip;
};

double scale(const IntPair fraction) {
Expand Down Expand Up @@ -302,6 +310,74 @@ get_path_data_string(const std::span<const std::vector<IntPair>> polygons,
return result;
}

std::vector<IntPair> get_rectangle_polygon(const Rectangle &rect) {
return {{rect.left, rect.top},
{rect.right, rect.top},
{rect.right, rect.bottom},
{rect.left, rect.bottom}};
}

/// The region's outline: the shape it came from where the file kept one, and
/// the union its bands cover where it did not.
std::string get_region_path_data(const Region &region, const Context &context) {
if (!region.polygons.empty()) {
return get_path_data_string(region.polygons, true, context);
}

std::vector<std::vector<IntPair>> polygons;
polygons.reserve(region.rectangles.size());
for (const Rectangle &rect : region.rectangles) {
polygons.push_back(get_rectangle_polygon(rect));
}
return get_path_data_string(polygons, true, context);
}

/// A file that sets the drawing area and then intersects the same rectangle
/// asks for a group that clips nothing; that one is dropped.
void intersect_clip(std::string path_data, GraphicsState &state) {
if (!state.clip.empty() && state.clip.back() == path_data) {
return;
}
state.clip.push_back(std::move(path_data));
}

/// Reconciles the open groups with the state's clip, keeping the prefix they
/// share. Every drawing action goes through here first.
void ensure_clip(Context &context) {
svg::SvgWriter &out = *context.out;
const std::vector<std::string> &clip = context.state.clip;

std::size_t common = 0;
while (common < context.written_clip.size() && common < clip.size() &&
context.written_clip[common] == clip[common]) {
++common;
}

while (context.written_clip.size() > common) {
out.write_element_end();
context.written_clip.pop_back();
}

for (std::size_t i = common; i < clip.size(); ++i) {
const std::string id =
"odr-clip-" + std::to_string(++context.element_count);

out.write_element_begin("clipPath");
out.write_attribute("id", id);
out.write_element_begin("path");
out.write_attribute("d", clip[i]);
// holes, where the region kept the shape it was rasterised from; its
// bands never overlap, so they union under the same rule
out.write_attribute("clip-rule", "evenodd");
out.write_element_end();
out.write_element_end();

out.write_element_begin("g");
out.write_attribute("clip-path", "url(#" + id + ")");
context.written_clip.push_back(clip[i]);
}
}

/// One path for all of them: the fill rule only cuts holes within a path.
void write_path(const std::span<const std::vector<IntPair>> polygons,
const bool fill, const LineInfo *line_info,
Expand Down Expand Up @@ -560,6 +636,9 @@ void pop_state(Context &context) {
state.text_fill_rgb = saved.state.text_fill_rgb;
state.text_fill_rgb_set = saved.state.text_fill_rgb_set;
}
if (saved.flags & PUSH_CLIPREGION) {
state.clip = saved.state.clip;
}
if (saved.flags & PUSH_TEXTALIGN) {
state.text_align = saved.state.text_align;
}
Expand Down Expand Up @@ -606,19 +685,23 @@ void translate_action(const ActionHeader &action_header, std::istream &in,
break;
case META_RECT_ACTION: {
const Rectangle action = read_rectangle(in);
ensure_clip(context);
write_rectangle(action, context);
} break;
case META_POLYLINE_ACTION: {
const auto [points, line_info] =
read_poly_line_action(in, action_header.vl);
ensure_clip(context);
write_path({&points, 1}, false, &line_info, context);
} break;
case META_POLYGON_ACTION: {
const auto [points] = read_polygon_action(in, action_header.vl);
ensure_clip(context);
write_path({&points, 1}, true, nullptr, context);
} break;
case META_POLYPOLYGON_ACTION: {
const auto [polygons] = read_poly_polygon_action(in, action_header.vl);
ensure_clip(context);
write_path(polygons, true, nullptr, context);
} break;
case META_BMP_ACTION:
Expand All @@ -629,24 +712,45 @@ void translate_action(const ActionHeader &action_header, std::istream &in,
case META_BMPEXSCALEPART_ACTION: {
const BitmapAction action =
read_bitmap_action(in, action_header.type, action_header.vl);
ensure_clip(context);
write_bitmap(action, context);
} break;
case META_CLIPREGION_ACTION: {
const auto [region, clip] = read_clip_region_action(in);
state.clip.clear();
if (clip && region) {
intersect_clip(get_region_path_data(*region, context), state);
}
} break;
case META_ISECTRECTCLIPREGION_ACTION: {
const Rectangle action = read_rectangle(in);
const std::vector<IntPair> polygon = get_rectangle_polygon(action);
intersect_clip(get_path_data_string({&polygon, 1}, true, context), state);
} break;
case META_ISECTREGIONCLIPREGION_ACTION: {
if (const std::optional<Region> region = read_region(in)) {
intersect_clip(get_region_path_data(*region, context), state);
}
} break;
case META_TEXTALIGN_ACTION:
state.text_align = read_text_align_action(in);
break;
case META_TEXT_ACTION: {
const TextAction action =
read_text_action(in, action_header.vl, state.encoding);
ensure_clip(context);
write_text(action.point, action.text, {}, 0, context);
} break;
case META_TEXTARRAY_ACTION: {
const TextArrayAction action =
read_text_array_action(in, action_header.vl, state.encoding);
ensure_clip(context);
write_text(action.point, action.text, action.dx_array, 0, context);
} break;
case META_STRETCHTEXT_ACTION: {
const StretchTextAction action =
read_stretch_text_action(in, action_header.vl, state.encoding);
ensure_clip(context);
write_text(action.point, action.text, {}, action.width, context);
} break;
default:
Expand Down Expand Up @@ -706,6 +810,10 @@ void svm::translate_to_svg(const SvmFile &file, std::ostream &out,
ODR_WARNING(logger, context.stack.size() << " pushes were never popped");
}

// whatever the last clip left open
context.state.clip.clear();
ensure_clip(context);

writer.write_element_end();
}

Expand Down
4 changes: 2 additions & 2 deletions test/data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ odr_test_data(
odr_test_data(
PATH "reference-output/odr-public"
URL "https://github.com/opendocument-app/OpenDocument.test.output.git"
REVISION "29c6270283527ff91c3ca125f67bd4260aa2acae")
REVISION "05441f24b4fcc9398b5115718a7d812ab4d63cea")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "898f1d823afdca5008ebb4a6579e0d4d9e412554")
REVISION "3ef61b792470da12cfd76e74afa1e0fad0812617")
Loading
Loading