diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e342b4..9059bf23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- A curve in a StarView metafile is drawn as a curve. The polygon flags that + say which of its points are bezier control points were read and dropped, so + every curve came out as a line through them. + - A StarView metafile's map mode is read in full: its unit, so a drawing that switches to twips or points is no longer off by the factor between them, and the relative map mode, which composes with the one before it rather than diff --git a/src/odr/internal/svm/PLAN.md b/src/odr/internal/svm/PLAN.md index b2135b1d..4d824050 100644 --- a/src/odr/internal/svm/PLAN.md +++ b/src/odr/internal/svm/PLAN.md @@ -37,8 +37,11 @@ Each stage is one pull request, stacked on the one before it. `TRANSPARENT` are done. `WALLPAPER` and `FLOATTRANSPARENT` are not: the first has a format of its own, the second nests a whole metafile. 8. **The map mode** (#772 defect 6) - done. -9. **Stretch.** Bézier flags (#772 defect 4), the `EPS` substitute metafile, - and version-1 (pre-`VCLMTF`) files via `SvmConverter.cxx`. +9. **Stretch.** Bézier flags (#772 defect 4) are done. Left: `WALLPAPER`, + `FLOATTRANSPARENT` and `EPS`, which all nest something of their own; the + `MASK` family; `MOVECLIPREGION`; `ZCOMPRESS`ed bitmaps; and version-1 + (pre-`VCLMTF`) files via `SvmConverter.cxx`. None of them occurs in the + corpus. The order follows what files actually contain, not the action list. Over 1125 metafiles harvested from the `odt`/`ods` fixtures: @@ -122,9 +125,10 @@ are worth writing down: What is left: `ZCOMPRESS`, a LibreOffice-only compression whose zlib stream would have to be inflated (miniz is already a dependency) before any of the above, and the `MASK` family, which stencils one colour through a bitmap. -- **Béziers are cheap once the flags are read.** A polygon flag of - `PolyFlags::Control` marks a control point, so a flagged polygon maps onto an - SVG path's `C` segments directly. The reader is the part that is missing. +- **Béziers were cheap once the flags were read** - this one is taken. A + polygon that carries curves is written *twice*, the second time with one + `PolyFlags` per point, and two control points between two corners are an + svg `C`. - **Gradients, hatches and dashes are declarative in SVG** — ``, ``, ``, `stroke-dasharray`. No rasterising, no tiling by hand. diff --git a/src/odr/internal/svm/svm_format.cpp b/src/odr/internal/svm/svm_format.cpp index 26a97322..42458da6 100644 --- a/src/odr/internal/svm/svm_format.cpp +++ b/src/odr/internal/svm/svm_format.cpp @@ -362,23 +362,39 @@ svm::Rectangle svm::read_rectangle(std::istream &in) { return result; } -std::vector svm::read_polygon(std::istream &in) { - std::vector result; +svm::Polygon svm::read_polygon(std::istream &in) { + Polygon result; std::uint16_t size; read_primitive(in, size); - result.resize(size); - for (auto &&p : result) { + result.points.resize(size); + for (auto &&p : result.points) { p = read_int_pair(in); } return result; } -std::vector> -svm::read_poly_polygon(std::istream &in) { - std::vector> result; +svm::Polygon svm::read_flagged_polygon(std::istream &in) { + read_version_length(in); + + Polygon result = read_polygon(in); + + bool has_flags{}; + read_primitive(in, has_flags); + if (has_flags) { + result.flags.resize(result.points.size()); + for (auto &&flag : result.flags) { + read_primitive(in, flag); + } + } + + return result; +} + +std::vector svm::read_poly_polygon(std::istream &in) { + std::vector result; std::uint16_t size; read_primitive(in, size); @@ -562,18 +578,20 @@ svm::PolyLineAction svm::read_poly_line_action(std::istream &in, const VersionLength &vl) { PolyLineAction result; - result.points = read_polygon(in); + result.polygon = read_polygon(in); if (vl.version >= 2) { result.line_info = read_line_info(in); } if (vl.version >= 3) { - bool has_flags; + bool has_flags{}; read_primitive(in, has_flags); + // the same polygon again, with what each point is; it replaces the + // corners-only one read above if (has_flags) { - // TODO flags not implemented + result.polygon = read_flagged_polygon(in); } } @@ -584,14 +602,14 @@ svm::PolygonAction svm::read_polygon_action(std::istream &in, const VersionLength &vl) { PolygonAction result; - result.points = read_polygon(in); + result.polygon = read_polygon(in); - if (vl.version >= 3) { - bool has_flags; + if (vl.version >= 2) { + bool has_flags{}; read_primitive(in, has_flags); if (has_flags) { - // TODO flags not implemented + result.polygon = read_flagged_polygon(in); } } @@ -605,11 +623,18 @@ svm::PolyPolygonAction svm::read_poly_polygon_action(std::istream &in, result.polygons = read_poly_polygon(in); if (vl.version >= 2) { - std::uint16_t complex_polygons; + std::uint16_t complex_polygons{}; read_primitive(in, complex_polygons); - if (complex_polygons > 0) { - // TODO complex not implemented + // each names one of the polygons above and replaces it with the same + // shape, curves included + for (std::uint16_t i = 0; i < complex_polygons; ++i) { + std::uint16_t index{}; + read_primitive(in, index); + Polygon polygon = read_flagged_polygon(in); + if (index < result.polygons.size()) { + result.polygons[index] = std::move(polygon); + } } } diff --git a/src/odr/internal/svm/svm_format.hpp b/src/odr/internal/svm/svm_format.hpp index a1aa2eff..ae845a1a 100644 --- a/src/odr/internal/svm/svm_format.hpp +++ b/src/odr/internal/svm/svm_format.hpp @@ -61,6 +61,15 @@ enum MetaFontStrikeout { STRIKEOUT_NONE = 0, }; +/// `PolyFlags`: what a polygon's point is. Two `CONTROL` points between two +/// corners are a bezier segment; everything else is a line. +enum MetaPolyFlags { + POLY_NORMAL = 0, + POLY_SMOOTH = 1, + POLY_CONTROL = 2, + POLY_SYMMETRIC = 3, +}; + /// `MapUnit`, the unit a map mode's coordinates are in. `MAP_RELATIVE` has /// none of its own: it composes with the map mode before it. enum MetaMapUnit { @@ -288,17 +297,24 @@ struct ArcAction final { IntPair end; }; -struct PolyLineAction final { +/// A polygon, and - where the file carried them - what its points are. +struct Polygon final { std::vector points; + /// One `PolyFlags` per point, or empty where every point is a corner. + std::vector flags; +}; + +struct PolyLineAction final { + Polygon polygon; LineInfo line_info; }; struct PolygonAction final { - std::vector points; + Polygon polygon; }; struct PolyPolygonAction final { - std::vector> polygons; + std::vector polygons; }; struct TextAction final { @@ -363,7 +379,7 @@ struct Hatch final { /// clip at all, is no region and reads as `std::nullopt`. struct Region final { std::vector rectangles; - std::vector> polygons; + std::vector polygons; }; /// A dib as something a browser reads. A metafile stores a dib *with* its @@ -425,8 +441,11 @@ std::string read_string_with_encoding(std::istream &in, TextEncoding encoding); VersionLength read_version_length(std::istream &in); IntPair read_int_pair(std::istream &in); Rectangle read_rectangle(std::istream &in); -std::vector read_polygon(std::istream &in); -std::vector> read_poly_polygon(std::istream &in); +Polygon read_polygon(std::istream &in); +/// `Polygon::Read`: the points again, and the flags behind them - what a +/// polygon carrying curves is written as. +Polygon read_flagged_polygon(std::istream &in); +std::vector read_poly_polygon(std::istream &in); Header read_header(std::istream &in); ActionHeader read_action_header(std::istream &in); diff --git a/src/odr/internal/svm/svm_to_svg.cpp b/src/odr/internal/svm/svm_to_svg.cpp index ef76960e..bf8e3853 100644 --- a/src/odr/internal/svm/svm_to_svg.cpp +++ b/src/odr/internal/svm/svm_to_svg.cpp @@ -367,10 +367,10 @@ void write_rectangle(const Rectangle &rect, const Context &context, out.write_element_end(); } -/// `svgwriter.cxx`'s `GetPathString`: one `M`, one `L` run, `Z` where closed. -std::string -get_path_data_string(const std::span> polygons, - const bool close, const Context &context) { +/// `svgwriter.cxx`'s `GetPathString`: one `M`, then runs of `L` and `C` - +/// three control points in a row are one bezier segment - `Z` where closed. +std::string get_path_data_string(const std::span polygons, + const bool close, const Context &context) { std::string result; const auto append_point = [&](const IntPair point) { @@ -380,7 +380,8 @@ get_path_data_string(const std::span> polygons, }; for (const auto &polygon : polygons) { - if (polygon.size() < 2) { + const std::vector &points = polygon.points; + if (points.size() < 2) { continue; } @@ -388,15 +389,28 @@ get_path_data_string(const std::span> polygons, result += " "; } result += "M "; - append_point(polygon.front()); - result += " L"; - for (const IntPair point : polygon | std::views::drop(1)) { - result += " "; - append_point(point); + append_point(points.front()); + + char mode = 0; + for (std::size_t i = 1; i < points.size();) { + // `GetPathString`'s test: two control points between two corners + const bool curve = i + 2 < polygon.flags.size() && + polygon.flags[i] == POLY_CONTROL && + polygon.flags[i + 1] == POLY_CONTROL && + polygon.flags[i + 2] != POLY_CONTROL; + if (mode != (curve ? 'C' : 'L')) { + mode = curve ? 'C' : 'L'; + result += curve ? " C" : " L"; + } + for (std::size_t n = 0; n < (curve ? 3U : 1U); ++n, ++i) { + result += " "; + append_point(points[i]); + } } + // a polyline that ends where it began is closed - if (close || (polygon.front().x == polygon.back().x && - polygon.front().y == polygon.back().y)) { + if (close || (points.front().x == points.back().x && + points.front().y == points.back().y)) { result += " Z"; } } @@ -404,11 +418,12 @@ get_path_data_string(const std::span> polygons, return result; } -std::vector get_rectangle_polygon(const Rectangle &rect) { - return {{rect.left, rect.top}, - {rect.right, rect.top}, - {rect.right, rect.bottom}, - {rect.left, rect.bottom}}; +Polygon 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 @@ -418,7 +433,7 @@ std::string get_region_path_data(const Region ®ion, const Context &context) { return get_path_data_string(region.polygons, true, context); } - std::vector> polygons; + std::vector polygons; polygons.reserve(region.rectangles.size()); for (const Rectangle &rect : region.rectangles) { polygons.push_back(get_rectangle_polygon(rect)); @@ -490,7 +505,7 @@ struct Bounds final { [[nodiscard]] double center_y() const { return (top + bottom) / 2; } }; -Bounds get_bounds(const std::span> polygons, +Bounds get_bounds(const std::span polygons, const Context &context) { Bounds result{std::numeric_limits::max(), std::numeric_limits::max(), @@ -498,7 +513,7 @@ Bounds get_bounds(const std::span> polygons, std::numeric_limits::lowest()}; for (const auto &polygon : polygons) { - for (const IntPair point : polygon) { + for (const IntPair point : polygon.points) { const double x = transform_x(point.x, context); const double y = transform_y(point.y, context); result.left = std::min(result.left, x); @@ -660,7 +675,7 @@ void write_hatch_pattern(const std::string &id, const Hatch &hatch, } /// Fills the shape with the paint @p fill names, which is written above it. -void write_filled_path(const std::span> polygons, +void write_filled_path(const std::span polygons, const std::string &fill, const Context &context) { svg::SvgWriter &out = *context.out; @@ -672,7 +687,7 @@ void write_filled_path(const std::span> polygons, out.write_element_end(); } -void write_gradient(const std::span> polygons, +void write_gradient(const std::span polygons, const Gradient &gradient, Context &context) { const Bounds bounds = get_bounds(polygons, context); if (bounds.left > bounds.right) { @@ -689,8 +704,8 @@ void write_gradient(const std::span> polygons, write_filled_path(polygons, "url(#" + id + ")", context); } -void write_hatch(const std::span> polygons, - const Hatch &hatch, Context &context) { +void write_hatch(const std::span polygons, const Hatch &hatch, + Context &context) { const std::string id = "odr-hatch-" + std::to_string(++context.element_count); write_hatch_pattern(id, hatch, context); @@ -699,7 +714,7 @@ void write_hatch(const std::span> polygons, /// The state's own colours, drawn through: `DrawTransparent` is the fill and /// the pen at a transparency, not a colour of its own. -void write_transparent(const std::span> polygons, +void write_transparent(const std::span polygons, const std::uint16_t transparence, const Context &context) { svg::SvgWriter &out = *context.out; @@ -713,9 +728,8 @@ void write_transparent(const std::span> polygons, } /// One path for all of them: the fill rule only cuts holes within a path. -void write_path(const std::span> polygons, - const bool fill, const LineInfo *line_info, - const Context &context) { +void write_path(const std::span polygons, const bool fill, + const LineInfo *line_info, const Context &context) { svg::SvgWriter &out = *context.out; out.write_element_begin("path"); @@ -1149,9 +1163,9 @@ void translate_action(const ActionHeader &action_header, std::istream &in, } break; case META_LINE_ACTION: { const LineAction action = read_line_action(in, action_header.vl); - const std::vector points{action.start, action.end}; + const Polygon polygon{{action.start, action.end}, {}}; ensure_clip(context); - write_path({&points, 1}, false, &action.line_info, context); + write_path({&polygon, 1}, false, &action.line_info, context); } break; case META_ROUNDRECT_ACTION: { const RoundRectangleAction action = read_round_rectangle_action(in); @@ -1174,24 +1188,24 @@ void translate_action(const ActionHeader &action_header, std::istream &in, case META_GRADIENT_ACTION: { const Rectangle rect = read_rectangle(in); const Gradient gradient = read_gradient(in); - const std::vector polygon = get_rectangle_polygon(rect); + const Polygon polygon = get_rectangle_polygon(rect); ensure_clip(context); write_gradient({&polygon, 1}, gradient, context); } break; case META_GRADIENTEX_ACTION: { - const std::vector> polygons = read_poly_polygon(in); + const std::vector polygons = read_poly_polygon(in); const Gradient gradient = read_gradient(in); ensure_clip(context); write_gradient(polygons, gradient, context); } break; case META_HATCH_ACTION: { - const std::vector> polygons = read_poly_polygon(in); + const std::vector polygons = read_poly_polygon(in); const Hatch hatch = read_hatch(in); ensure_clip(context); write_hatch(polygons, hatch, context); } break; case META_TRANSPARENT_ACTION: { - const std::vector> polygons = read_poly_polygon(in); + const std::vector polygons = read_poly_polygon(in); std::uint16_t transparence{}; read_primitive(in, transparence); ensure_clip(context); @@ -1203,15 +1217,15 @@ void translate_action(const ActionHeader &action_header, std::istream &in, write_rectangle(action, context); } break; case META_POLYLINE_ACTION: { - const auto [points, line_info] = + const auto [polygon, line_info] = read_poly_line_action(in, action_header.vl); ensure_clip(context); - write_path({&points, 1}, false, &line_info, context); + write_path({&polygon, 1}, false, &line_info, context); } break; case META_POLYGON_ACTION: { - const auto [points] = read_polygon_action(in, action_header.vl); + const auto [polygon] = read_polygon_action(in, action_header.vl); ensure_clip(context); - write_path({&points, 1}, true, nullptr, context); + write_path({&polygon, 1}, true, nullptr, context); } break; case META_POLYPOLYGON_ACTION: { const auto [polygons] = read_poly_polygon_action(in, action_header.vl); @@ -1238,7 +1252,7 @@ void translate_action(const ActionHeader &action_header, std::istream &in, } break; case META_ISECTRECTCLIPREGION_ACTION: { const Rectangle action = read_rectangle(in); - const std::vector polygon = get_rectangle_polygon(action); + const Polygon polygon = get_rectangle_polygon(action); intersect_clip(get_path_data_string({&polygon, 1}, true, context), state); } break; case META_ISECTREGIONCLIPREGION_ACTION: { diff --git a/test/src/internal/svm/svm_test.cpp b/test/src/internal/svm/svm_test.cpp index 90a3f5b0..da4086c5 100644 --- a/test/src/internal/svm/svm_test.cpp +++ b/test/src/internal/svm/svm_test.cpp @@ -60,6 +60,31 @@ class SvmBuilder final { return *this; } + /// `Polygon::Read`: the polygon again, with what each of its points is. + SvmBuilder &flagged_polygon( + const std::vector> &points, + const std::vector &flags) { + begin().polygon(points).u8(1); + for (const std::uint8_t flag : flags) { + u8(flag); + } + return end(); + } + + /// A default `LineInfo`, which a version 2 polyline carries. + SvmBuilder &line_info() { + return begin(3) + .u16(1) // solid + .i32(0) // width + .u16(0) // dash count + .i32(0) // dash length + .u16(0) // dot count + .i32(0) // dot length + .i32(0) // distance + .u16(0) // join + .end(); + } + /// The font the text actions below draw with, at @p size. SvmBuilder & font(const std::string &family, const std::int32_t size, @@ -1062,3 +1087,59 @@ TEST(SvmToSvg, a_pop_restores_the_map_mode) { EXPECT_NE(std::string::npos, svg.find("width=\"100\" height=\"100\"")); } + +/// Two control points between two corners are a bezier segment, which is a +/// `C` - the same path LibreOffice writes for the same polygon. +TEST(SvmToSvg, a_polyline_with_flags_curves) { + const std::vector> points = { + {100, 700}, {300, 100}, {700, 100}, {900, 700}}; + + const std::string svg = translate( + SvmBuilder() + .action(svm::META_POLYLINE_ACTION, 3) + .polygon(points) + .line_info() + .u8(1) // the flagged polygon follows + .flagged_polygon(points, {svm::POLY_NORMAL, svm::POLY_CONTROL, + svm::POLY_CONTROL, svm::POLY_NORMAL}) + .end() + .file()); + + EXPECT_NE(std::string::npos, + svg.find("d=\"M 100,700 C 300,100 700,100 900,700\"")); +} + +/// Without them the same points are a line through what were the curve's +/// control points. +TEST(SvmToSvg, a_polyline_without_flags_is_lines) { + const std::string svg = + translate(SvmBuilder() + .action(svm::META_POLYLINE_ACTION, 3) + .polygon({{100, 700}, {300, 100}, {700, 100}, {900, 700}}) + .line_info() + .u8(0) + .end() + .file()); + + EXPECT_NE(std::string::npos, + svg.find("d=\"M 100,700 L 300,100 700,100 900,700\"")); +} + +/// A poly-polygon names the polygons it wants replaced by the same shape with +/// its curves. +TEST(SvmToSvg, a_complex_poly_polygon_replaces_its_polygon) { + const std::string svg = + translate(SvmBuilder() + .action(svm::META_POLYPOLYGON_ACTION, 2) + .u16(1) // polygons + .polygon({{0, 0}, {10, 0}, {10, 10}, {0, 10}}) + .u16(1) // complex polygons + .u16(0) // the index of the one to replace + .flagged_polygon({{0, 0}, {5, 0}, {5, 5}, {0, 5}}, + {svm::POLY_NORMAL, svm::POLY_CONTROL, + svm::POLY_CONTROL, svm::POLY_NORMAL}) + .end() + .file()); + + EXPECT_NE(std::string::npos, svg.find("d=\"M 0,0 C 5,0 5,5 0,5 Z\"")); +}