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 @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- An ODF custom shape is drawn as the shape its `draw:enhanced-geometry`
describes rather than as its bounding box: `draw:enhanced-path`,
`draw:equation`, `draw:modifiers` and the two mirror attributes. Closes #159.

- ODF draws the shape elements it used to drop whole: `draw:path`,
`draw:polygon`, `draw:polyline`, `draw:regular-polygon`, `draw:connector`,
`draw:ellipse`, `draw:measure` and `draw:caption`. New `path()` on
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,8 @@ void html::translate_custom_shape(const Element &element,
"absolute;top:0;left:0;padding:inherit;"));
HtmlAttributesVector attributes{
{"d", path->data},
// A ring is two subpaths, and only even-odd leaves its hole.
{"fill-rule", "evenodd"},
// The view box scales, and not evenly; the stroke must not.
{"vector-effect", "non-scaling-stroke"}};
// An outline that never closes is a line, which svg would else fill as if
Expand Down
21 changes: 17 additions & 4 deletions src/odr/internal/odf/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,20 @@ turn, so the large-arc flag is never needed and a full `U … 0 360` — which o
svg `A` cannot express — still draws; `F` and `S` are read and dropped, since
painting one subpath differently is more than one `d` can say.

### 4 — enhanced geometry, rendered
### 4 — enhanced geometry, rendered — landed

Stage 3 wired into `CustomShape::path()`: `svg:viewBox`, `draw:modifiers`,
`draw:mirror-vertical` / `-horizontal`, `draw:text-areas` for where the shape's
text goes. Closes #159. This is the stage that regenerates reference output.
Stage 3 wired into `CustomShape::path()`: `svg:viewBox`, `draw:modifiers`, the
`draw:equation` children resolved on demand and memoised, and
`draw:mirror-horizontal` / `-vertical` folded into the coordinates as they are
written. Closes #159.

Still open, and deliberately: `draw:text-areas`, so a shape's text is laid out
in the whole box rather than the region the geometry reserves for it;
`draw:handle`, which only matters to an editor; and `F`/`S`, which want one
subpath painted differently from the rest.

`hasstroke` and `hasfill` are always true — the geometry reader has no style
in hand — and no corpus formula reads them.

### 5 — `draw:object` charts

Expand All @@ -155,3 +164,7 @@ nothing with stages 1–4 but the `draw:frame` it hangs off.
written by the producer, and drawing that is the whole win. Re-routing a
connector between the shapes it names is layout, not decoding.
- **Editing any of this.** The editor is text-content only (`AGENTS.md`).
- **`draw:type`'s preset shapes.** A named type with no `draw:enhanced-path`
would need libreoffice's preset table; all 350 in the corpus write the path.
- **`draw:marker`** (28 in the corpus): the arrowheads a stroke ends with, an
svg `marker` and a separate piece of style work.
9 changes: 7 additions & 2 deletions src/odr/internal/odf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,18 @@ Roughly ordered by importance.
- [x] caption (`draw:caption`; the box, not the tail)
- [ ] 3-D scene (`dr3d:scene`)
- [x] custom shapes (bounding box, fill/stroke) #159
- [ ] enhanced geometry / shape path rendering
- [x] enhanced geometry (`draw:enhanced-path`, `draw:equation`,
`draw:modifiers`, `draw:mirror-horizontal` / `-vertical`)
- [ ] `draw:text-areas` (text is laid out in the whole box)
- [ ] `draw:handle` (the interactive control points)
- [ ] `F` / `S`: one subpath painted differently from the rest
- [x] graphic style: stroke width/color, fill color, vertical align, text wrap
- [ ] gradient and hatch fills, `draw:opacity` / `draw:opacity-name`, and the
dash a `draw:stroke` names (a solid `draw:fill-color` and a solid line)
- [ ] arrowheads (`draw:marker`, `draw:marker-start` / `-end`)
- [x] transform (`draw:transform`, its operation list composed to one matrix)
- [ ] mirror (`style:mirror`, `draw:mirror-horizontal` / `-vertical`)
- [ ] mirror (`style:mirror`, and `draw:mirror-*` on a shape with no
enhanced geometry)
- [x] page layout (size, orientation, margins)
- [ ] annotations (`office:annotation`)

Expand Down
24 changes: 18 additions & 6 deletions src/odr/internal/odf/odf_enhanced_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,27 @@ class EnhancedPathParser : private Scanner {
m_out += util::number::to_string_significant(value == 0 ? 0 : value, 7);
}

/// The pen tracks the unmirrored geometry; only what is written is
/// reflected, so the angles and radii above are computed once.
void write_point(const double x, const double y) {
write(x);
write(y);
write(m_context->mirror_horizontal ? m_context->left + m_context->right - x
: x);
write(m_context->mirror_vertical ? m_context->top + m_context->bottom - y
: y);
m_x = x;
m_y = y;
}

/// A reflection in one axis reverses the way an arc turns; in both, it does
/// not.
void write_sweep(const bool clockwise) {
m_out += " 0 0 ";
m_out += (clockwise !=
(m_context->mirror_horizontal != m_context->mirror_vertical))
? '1'
: '0';
}

/// An elliptical arc from @p from to @p to degrees, in segments of at most
/// a half turn, so a full turn — which one `A` cannot express — still draws.
void write_arc(const double cx, const double cy, const double rx,
Expand All @@ -326,8 +340,7 @@ class EnhancedPathParser : private Scanner {
write('A');
write(rx);
write(ry);
m_out += " 0 0 ";
m_out += swept < 0 ? '0' : '1';
write_sweep(swept >= 0);
write_point(end[0], end[1]);
}
}
Expand Down Expand Up @@ -413,8 +426,7 @@ class EnhancedPathParser : private Scanner {
write('A');
write(rx);
write(ry);
m_out += " 0 0 ";
m_out += (descending == x_first) ? '1' : '0';
write_sweep(descending == x_first);
write_point(*x, *y);
return true;
}
Expand Down
3 changes: 3 additions & 0 deletions src/odr/internal/odf/odf_enhanced_geometry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ struct EnhancedGeometryContext final {
bool has_fill{true};
/// `draw:modifiers`, which `$0`, `$1`, … index.
std::vector<double> modifiers;
/// `draw:mirror-horizontal` / `-vertical`, reflected about the view box.
bool mirror_horizontal{false};
bool mirror_vertical{false};
};

/// Resolves a `?name` reference to the `draw:equation` it names.
Expand Down
98 changes: 98 additions & 0 deletions src/odr/internal/odf/odf_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <odr/document_element.hpp>

#include <odr/internal/odf/odf_enhanced_geometry.hpp>
#include <odr/internal/odf/odf_scanner.hpp>
#include <odr/internal/util/math_util.hpp>
#include <odr/internal/util/number_util.hpp>
Expand All @@ -12,7 +13,10 @@
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <functional>
#include <map>
#include <numbers>
#include <set>
#include <string>
#include <vector>

Expand Down Expand Up @@ -455,6 +459,96 @@ std::optional<DrawingPath> read_regular_polygon(const pugi::xml_node node) {
.height = view_box_size};
}

/// Space-separated numbers, as `draw:modifiers` (19.214) writes them.
std::vector<double> read_numbers(const pugi::xml_attribute attribute) {
Scanner in(attribute.value());
std::vector<double> result;
while (true) {
const std::optional<double> value = in.read_number();
if (!value.has_value()) {
break;
}
result.push_back(*value);
}
return result;
}

/// The `draw:equation` children by name, resolved on demand and memoised. A
/// reference that cycles resolves to nothing rather than recursing.
class Equations {
public:
Equations(const pugi::xml_node geometry,
const EnhancedGeometryContext &context)
: m_geometry{geometry}, m_context{&context} {}

std::optional<double> operator()(const std::string_view name) {
const std::string key(name);
if (const auto it = m_resolved.find(key); it != m_resolved.end()) {
return it->second;
}
if (!m_resolving.insert(key).second) {
return {};
}
std::optional<double> value;
for (const pugi::xml_node equation : m_geometry.children("draw:equation")) {
if (key == equation.attribute("draw:name").value()) {
value = evaluate_formula(equation.attribute("draw:formula").value(),
*m_context, std::ref(*this));
break;
}
}
m_resolving.erase(key);
m_resolved.emplace(key, value);
return value;
}

private:
pugi::xml_node m_geometry;
const EnhancedGeometryContext *m_context{nullptr};
std::map<std::string, std::optional<double>> m_resolved;
std::set<std::string> m_resolving;
};

/// `draw:enhanced-geometry` (10.6.1) on a `draw:custom-shape`, resolved to the
/// outline its `draw:enhanced-path` traces.
std::optional<DrawingPath> read_enhanced_geometry(const pugi::xml_node node) {
const pugi::xml_node geometry = node.child("draw:enhanced-geometry");
if (!geometry) {
return {};
}
const pugi::xml_attribute path = geometry.attribute("draw:enhanced-path");
if (!path) {
return {};
}

EnhancedGeometryContext context;
if (const std::optional<ViewBox> view_box = read_view_box(geometry)) {
context.left = view_box->x;
context.top = view_box->y;
context.right = view_box->x + view_box->width;
context.bottom = view_box->y + view_box->height;
}
context.logical_width = context.right - context.left;
context.logical_height = context.bottom - context.top;
context.modifiers = read_numbers(geometry.attribute("draw:modifiers"));
context.mirror_horizontal =
geometry.attribute("draw:mirror-horizontal").as_bool(false);
context.mirror_vertical =
geometry.attribute("draw:mirror-vertical").as_bool(false);

Equations equations(geometry, context);
const std::optional<std::string> data =
convert_enhanced_path(path.value(), context, std::ref(equations));
if (!data.has_value()) {
return {};
}
return DrawingPath{.data = *data,
.x = context.left,
.y = context.top,
.width = context.right - context.left,
.height = context.bottom - context.top};
}

/// A `draw:circle` / `draw:ellipse` that `draw:kind` (19.212) cuts.
std::optional<DrawingPath> read_elliptical_kind(const pugi::xml_node node) {
const std::string_view kind = node.attribute("draw:kind").value();
Expand Down Expand Up @@ -587,6 +681,10 @@ std::optional<DrawingPath> odf::read_path(const pugi::xml_node node) {
return odf::read_elliptical_kind(node);
}

if (name == "draw:custom-shape") {
return odf::read_enhanced_geometry(node);
}

return {};
}

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 "7ed0b50514bbd3a17e35254139d1ba7bce2f32cc")
REVISION "cb5b29143d4cf88340ccbca106bd8d3ab39429bf")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "400a73efcd9ed311a3118de84687b7922af91fb3")
REVISION "c3ec7fd812443b8d0520259cb0e3b10299a6a8da")
Loading