diff --git a/CHANGELOG.md b/CHANGELOG.md index 988c57be..9c909d8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- A StarView metafile draws its gradients, hatches and transparent shapes: + `GRADIENT`, `GRADIENTEX`, `HATCH` and `TRANSPARENT`. + - A spreadsheet decodes in less memory: 626 MB peak instead of 914 MB on a 297 MB `content.xml`. Rendered output is unchanged. diff --git a/src/odr/internal/svm/PLAN.md b/src/odr/internal/svm/PLAN.md index 534dcff8..193a50e6 100644 --- a/src/odr/internal/svm/PLAN.md +++ b/src/odr/internal/svm/PLAN.md @@ -33,8 +33,9 @@ Each stage is one pull request, stacked on the one before it. which stencils one colour through a bitmap, and `ZCOMPRESS`, which needs inflating first, are still open. See the shortcut below. 6. **Primitives** - done. -7. **Fills and transparency.** `GRADIENT`, `GRADIENTEX`, `HATCH`, - `WALLPAPER`, `TRANSPARENT`, `FLOATTRANSPARENT`. +7. **Fills and transparency** - `GRADIENT`, `GRADIENTEX`, `HATCH` and + `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's unit** (#772 defect 6), see below. 9. **Stretch.** Bézier flags (#772 defect 4), the `EPS` substitute metafile, and version-1 (pre-`VCLMTF`) files via `SvmConverter.cxx`. diff --git a/src/odr/internal/svm/svm_format.cpp b/src/odr/internal/svm/svm_format.cpp index a0bd70c1..26a97322 100644 --- a/src/odr/internal/svm/svm_format.cpp +++ b/src/odr/internal/svm/svm_format.cpp @@ -830,6 +830,66 @@ svm::BitmapAction svm::read_bitmap_action(std::istream &in, return result; } +std::uint32_t svm::read_object_color(std::istream &in) { + // `COL_NAME_USER`: the id that means three channels follow + constexpr std::uint16_t color_name_user = 0x8000; + // the palette `GenericTypeSerializer::readColor` indexes, its system colours + // resolved as it resolves them + static constexpr std::array palette = { + 0x000000, 0x000080, 0x008000, 0x008080, 0x800000, 0x800080, 0x808000, + 0x808080, 0xc0c0c0, 0x0000ff, 0x00ff00, 0x00ffff, 0xff0000, 0xff00ff, + 0xffff00, 0xffffff, 0xffffff, 0x000000, 0xffffff, 0x000000, 0x000000, + 0xffffff, 0x000000, 0xffffff, 0x000000, 0xc0c0c0, 0xffffff, 0x808080, + 0xc0c0c0, 0xffffff, 0x000000}; + + std::uint16_t name{}; + read_primitive(in, name); + + if ((name & color_name_user) == 0) { + return name < palette.size() ? palette[name] : 0; + } + + std::uint16_t red{}; + std::uint16_t green{}; + std::uint16_t blue{}; + read_primitive(in, red); + read_primitive(in, green); + read_primitive(in, blue); + return static_cast(red >> 8) << 16 | + static_cast(green >> 8) << 8 | + static_cast(blue >> 8); +} + +svm::Gradient svm::read_gradient(std::istream &in) { + Gradient result; + + read_version_length(in); + read_primitive(in, result.style); + result.start_color = read_object_color(in); + result.end_color = read_object_color(in); + read_primitive(in, result.angle); + read_primitive(in, result.border); + read_primitive(in, result.offset_x); + read_primitive(in, result.offset_y); + read_primitive(in, result.start_intensity); + read_primitive(in, result.end_intensity); + read_primitive(in, result.step_count); + + return result; +} + +svm::Hatch svm::read_hatch(std::istream &in) { + Hatch result; + + read_version_length(in); + read_primitive(in, result.style); + result.color = read_object_color(in); + read_primitive(in, result.distance); + read_primitive(in, result.angle); + + return result; +} + std::optional svm::read_region(std::istream &in) { const VersionLength vl = read_version_length(in); std::uint16_t content_version{}; diff --git a/src/odr/internal/svm/svm_format.hpp b/src/odr/internal/svm/svm_format.hpp index 9096c789..48cb576a 100644 --- a/src/odr/internal/svm/svm_format.hpp +++ b/src/odr/internal/svm/svm_format.hpp @@ -61,6 +61,23 @@ enum MetaFontStrikeout { STRIKEOUT_NONE = 0, }; +/// `awt::GradientStyle`. +enum MetaGradientStyle { + GRADIENT_LINEAR = 0, + GRADIENT_AXIAL = 1, + GRADIENT_RADIAL = 2, + GRADIENT_ELLIPTICAL = 3, + GRADIENT_SQUARE = 4, + GRADIENT_RECT = 5, +}; + +/// `HatchStyle`: one set of lines, two crossing, or three. +enum MetaHatchStyle { + HATCH_SINGLE = 0, + HATCH_DOUBLE = 1, + HATCH_TRIPLE = 2, +}; + /// `LineStyle`, what a `LineInfo` draws with. enum MetaLineStyle { LINE_NONE = 0, @@ -294,6 +311,33 @@ struct TextRectangleAction final { std::uint16_t style{}; }; +/// `Gradient`: the two colours, which way the ramp runs, and where it sits. +struct Gradient final { + std::uint16_t style{}; + std::uint32_t start_color{}; + std::uint32_t end_color{}; + /// Tenths of a degree, counter-clockwise. + std::uint16_t angle{}; + /// Percent of the ramp the start colour holds before it ramps. + std::uint16_t border{}; + /// Percent of the bounds, where a complex gradient's centre sits. + std::uint16_t offset_x{}; + std::uint16_t offset_y{}; + /// Percent, what the colours are scaled by. + std::uint16_t start_intensity{100}; + std::uint16_t end_intensity{100}; + std::uint16_t step_count{}; +}; + +/// `Hatch`: the lines drawn across a shape. +struct Hatch final { + std::uint16_t style{}; + std::uint32_t color{}; + std::int32_t distance{}; + /// Tenths of a degree, counter-clockwise. + std::uint16_t angle{}; +}; + /// 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 @@ -394,6 +438,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 colour *inside* an object, which is not the plain `uint32` an action's +/// own colour is: `GenericTypeSerializer::readColor` reads a name id, and only +/// the user one carries three 16-bit channels behind it. +std::uint32_t read_object_color(std::istream &in); +Gradient read_gradient(std::istream &in); +Hatch read_hatch(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 read_region(std::istream &in); diff --git a/src/odr/internal/svm/svm_to_svg.cpp b/src/odr/internal/svm/svm_to_svg.cpp index e94c3f5e..892d2d6c 100644 --- a/src/odr/internal/svm/svm_to_svg.cpp +++ b/src/odr/internal/svm/svm_to_svg.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -391,6 +392,246 @@ void ensure_clip(Context &context) { } } +/// A percent out of the file, clamped: it is drawn with, not validated. +double percent(const std::uint16_t value) { + return std::min(value, 100) / 100.0; +} + +/// The bounds of what is about to be filled, in the drawing's own coordinates. +struct Bounds final { + double left{}; + double top{}; + double right{}; + double bottom{}; + + [[nodiscard]] double width() const { return right - left; } + [[nodiscard]] double height() const { return bottom - top; } + [[nodiscard]] double center_x() const { return (left + right) / 2; } + [[nodiscard]] double center_y() const { return (top + bottom) / 2; } +}; + +Bounds get_bounds(const std::span> polygons, + const Context &context) { + Bounds result{std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::lowest(), + std::numeric_limits::lowest()}; + + for (const auto &polygon : polygons) { + for (const IntPair point : polygon) { + const double x = transform_x(point.x, context); + const double y = transform_y(point.y, context); + result.left = std::min(result.left, x); + result.top = std::min(result.top, y); + result.right = std::max(result.right, x); + result.bottom = std::max(result.bottom, y); + } + } + + return result; +} + +/// A gradient end's colour, scaled by the intensity the file gives it. +std::string get_gradient_color_string(const std::uint32_t color, + const std::uint16_t intensity) { + const auto scale = [&](const std::uint32_t shift) { + return static_cast((color >> shift & 0xff) * + percent(intensity)); + }; + return get_svg_color_string(scale(16) << 16 | scale(8) << 8 | scale(0)); +} + +void write_gradient_stop(svg::SvgWriter &out, const double offset, + const std::string &color) { + out.write_element_begin("stop"); + out.write_attribute("offset", offset); + out.write_attribute("stop-color", color); + out.write_element_end(); +} + +/// `Gradient::GetBoundRect`: a linear ramp runs across the bounds grown so +/// that turning it still covers them, from the top of that to the bottom, +/// turned about the centre - which is `(sin, cos)` of the angle. +void write_linear_gradient(const std::string &id, const Gradient &gradient, + const Bounds &bounds, const Context &context) { + svg::SvgWriter &out = *context.out; + + const double angle = gradient.angle % 3600 * std::numbers::pi / 1800; + const double grown = bounds.height() * std::abs(std::cos(angle)) + + bounds.width() * std::abs(std::sin(angle)); + const double x = std::sin(angle) * grown / 2; + const double y = std::cos(angle) * grown / 2; + + out.write_element_begin("linearGradient"); + out.write_attribute("id", id); + out.write_attribute("gradientUnits", "userSpaceOnUse"); + out.write_attribute("x1", bounds.center_x() - x); + out.write_attribute("y1", bounds.center_y() - y); + out.write_attribute("x2", bounds.center_x() + x); + out.write_attribute("y2", bounds.center_y() + y); + + const std::string start = + get_gradient_color_string(gradient.start_color, gradient.start_intensity); + const std::string end = + get_gradient_color_string(gradient.end_color, gradient.end_intensity); + const double border = percent(gradient.border); + + if (gradient.style == GRADIENT_AXIAL) { + // `DrawLinearGradient` swaps the two for an axial ramp: the end colour is + // what both ends of the axis get, and the start colour the middle + write_gradient_stop(out, 0, end); + if (border > 0) { + write_gradient_stop(out, border / 2, end); + } + write_gradient_stop(out, 0.5, start); + if (border > 0) { + write_gradient_stop(out, 1 - border / 2, end); + } + write_gradient_stop(out, 1, end); + } else { + write_gradient_stop(out, 0, start); + if (border > 0) { + write_gradient_stop(out, border, start); + } + write_gradient_stop(out, 1, end); + } + + out.write_element_end(); +} + +/// The complex styles, which vcl draws as rings shrinking towards the centre: +/// the start colour is the outside and the end colour the middle. `SQUARE` +/// and `RECT` shrink a rectangle rather than an ellipse, which svg has no +/// gradient for - they come out as the ellipse they are closest to. +void write_radial_gradient(const std::string &id, const Gradient &gradient, + const Bounds &bounds, const Context &context) { + svg::SvgWriter &out = *context.out; + + const bool round = gradient.style == GRADIENT_RADIAL; + const double radius_x = round + ? std::hypot(bounds.width(), bounds.height()) / 2 + : bounds.width() * std::numbers::sqrt2 / 2; + const double radius_y = + round ? radius_x : bounds.height() * std::numbers::sqrt2 / 2; + + out.write_element_begin("radialGradient"); + out.write_attribute("id", id); + out.write_attribute("gradientUnits", "userSpaceOnUse"); + out.write_attribute("cx", bounds.left + + bounds.width() * percent(gradient.offset_x)); + out.write_attribute("cy", bounds.top + + bounds.height() * percent(gradient.offset_y)); + // the circle the transform below stretches to @ref radius_x + out.write_attribute("r", radius_y * (1 - percent(gradient.border))); + if (radius_x != radius_y) { + out.write_attribute( + "gradientTransform", + "matrix(" + svg::format_number(radius_x / radius_y) + " 0 0 1 " + + svg::format_number(bounds.center_x() * (1 - radius_x / radius_y)) + + " 0)"); + } + + write_gradient_stop( + out, 0, + get_gradient_color_string(gradient.end_color, gradient.end_intensity)); + write_gradient_stop(out, 1, + get_gradient_color_string(gradient.start_color, + gradient.start_intensity)); + out.write_element_end(); +} + +/// The hatch lines, as a tile the fill repeats. vcl turns them +/// counter-clockwise, svg clockwise. +void write_hatch_pattern(const std::string &id, const Hatch &hatch, + const Context &context) { + svg::SvgWriter &out = *context.out; + + const double distance = + std::max(transform_width(hatch.distance, context), 1.0); + + out.write_element_begin("pattern"); + out.write_attribute("id", id); + out.write_attribute("patternUnits", "userSpaceOnUse"); + out.write_attribute("width", distance); + out.write_attribute("height", distance); + out.write_attribute("patternTransform", + "rotate(" + svg::format_number(hatch.angle / -10.0) + + ")"); + + const auto line = [&](const std::string &path_data) { + out.write_element_begin("path"); + out.write_attribute("d", path_data); + out.write_style("stroke", get_svg_color_string(hatch.color)); + out.write_style("vector-effect", "non-scaling-stroke"); + out.write_style("fill", "none"); + out.write_element_end(); + }; + + const std::string size = svg::format_number(distance); + line("M 0,0 L " + size + ",0"); + if (hatch.style == HATCH_DOUBLE || hatch.style == HATCH_TRIPLE) { + line("M 0,0 L 0," + size); + } + if (hatch.style == HATCH_TRIPLE) { + line("M 0,0 L " + size + "," + size); + } + + out.write_element_end(); +} + +/// Fills the shape with the paint @p fill names, which is written above it. +void write_filled_path(const std::span> polygons, + const std::string &fill, const Context &context) { + svg::SvgWriter &out = *context.out; + + out.write_element_begin("path"); + out.write_attribute("d", get_path_data_string(polygons, true, context)); + out.write_style("fill", fill); + out.write_style("fill-rule", "evenodd"); + out.write_style("stroke", "none"); + out.write_element_end(); +} + +void write_gradient(const std::span> polygons, + const Gradient &gradient, Context &context) { + const Bounds bounds = get_bounds(polygons, context); + if (bounds.left > bounds.right) { + return; + } + + const std::string id = + "odr-gradient-" + std::to_string(++context.element_count); + if (gradient.style == GRADIENT_LINEAR || gradient.style == GRADIENT_AXIAL) { + write_linear_gradient(id, gradient, bounds, context); + } else { + write_radial_gradient(id, gradient, bounds, context); + } + write_filled_path(polygons, "url(#" + id + ")", 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); + write_filled_path(polygons, "url(#" + id + ")", context); +} + +/// 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, + const std::uint16_t transparence, + const Context &context) { + svg::SvgWriter &out = *context.out; + + out.write_element_begin("path"); + out.write_attribute("d", get_path_data_string(polygons, true, context)); + write_shape_style(out, context, true); + // the shape as a whole, not its fill and its stroke separately + out.write_style("opacity", 1 - percent(transparence)); + out.write_element_end(); +} + /// 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, @@ -854,6 +1095,32 @@ void translate_action(const ActionHeader &action_header, std::istream &in, ensure_clip(context); write_arc(action, get_arc_kind(action_header.type), context); } break; + case META_GRADIENT_ACTION: { + const Rectangle rect = read_rectangle(in); + const Gradient gradient = read_gradient(in); + const std::vector 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 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 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); + std::uint16_t transparence{}; + read_primitive(in, transparence); + ensure_clip(context); + write_transparent(polygons, transparence, context); + } break; case META_RECT_ACTION: { const Rectangle action = read_rectangle(in); ensure_clip(context); diff --git a/test/src/internal/svm/svm_test.cpp b/test/src/internal/svm/svm_test.cpp index 9d5108d2..6c51aaaa 100644 --- a/test/src/internal/svm/svm_test.cpp +++ b/test/src/internal/svm/svm_test.cpp @@ -110,6 +110,36 @@ class SvmBuilder final { .end(); } + /// A colour *inside* an object, which is not the plain `uint32` an action's + /// own colour is: a name id, and three 16-bit channels behind the user one. + SvmBuilder &object_color(const std::uint32_t rgb) { + return u16(0x8000) + .u16(static_cast((rgb >> 16 & 0xff) << 8)) + .u16(static_cast((rgb >> 8 & 0xff) << 8)) + .u16(static_cast((rgb & 0xff) << 8)); + } + + SvmBuilder &gradient(const std::uint16_t style, const std::uint32_t start, + const std::uint32_t end, const std::uint16_t angle = 0) { + begin().u16(style).object_color(start).object_color(end); + return u16(angle) + .u16(0) // border + .u16(50) // offset x + .u16(50) // offset y + .u16(100) // start intensity + .u16(100) // end intensity + .u16(0) // step count + .end(); + } + + /// A poly-polygon of one rectangle. + SvmBuilder &poly_rectangle(const std::int32_t left, const std::int32_t top, + const std::int32_t right, + const std::int32_t bottom) { + return u16(1).polygon( + {{left, top}, {right, top}, {right, bottom}, {left, bottom}}); + } + /// A region of one band, the shape `ReadRegion` reads a rectangle as. SvmBuilder ®ion(const std::int32_t left, const std::int32_t top, const std::int32_t right, const std::int32_t bottom) { @@ -829,3 +859,129 @@ TEST(SvmToSvg, a_point_and_a_pixel_are_dots) { svg.find("d=\"M 5,6 Z\" style=\"stroke-linecap:round;" "stroke:rgb(0,0,255)")); } + +/// The ramp runs from the top of the bounds to the bottom, turned about the +/// centre - the same vector LibreOffice's own export writes. +TEST(SvmToSvg, linear_gradient) { + const std::string svg = + translate(SvmBuilder() + .action(svm::META_GRADIENT_ACTION) + .rectangle(100, 100, 400, 400) + .gradient(svm::GRADIENT_LINEAR, 0xff0000, 0x0000ff) + .end() + .file()); + + EXPECT_NE(std::string::npos, + svg.find("