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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- 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
`CustomShape`, mirrored in the JNI, Apple and Python bindings.

- An ODF `draw:circle` is drawn as an ellipse rather than a circle inscribed in
its box.

- An ODF drawing shape is drawn where its `draw:transform` puts it. New
`transform()` on `Frame`, `Rect`, `Line`, `Circle` and `CustomShape`,
mirrored in the JNI, Apple and Python bindings.
Expand Down
16 changes: 16 additions & 0 deletions apple/include/OdrCoreObjC/ODRDocumentElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,20 @@ NS_SWIFT_NAME(TableCell)
@property(nonatomic, readonly) ODRTableCellStyle *style;
@end

/// `odr::DrawingPath`: an outline, in the user-space box `x`/`y`/`width`/
/// `height` that the shape's own box stretches to.
NS_SWIFT_NAME(DrawingPath)
@interface ODRDrawingPath : NSObject
@property(nonatomic, readonly, copy) NSString *data;
@property(nonatomic, readonly) double x;
@property(nonatomic, readonly) double y;
@property(nonatomic, readonly) double width;
@property(nonatomic, readonly) double height;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end

/// `odr::DrawingTransform`.
NS_SWIFT_NAME(DrawingTransform)
@interface ODRDrawingTransform : NSObject
Expand Down Expand Up @@ -318,6 +332,8 @@ NS_SWIFT_NAME(CustomShape)
@property(nonatomic, readonly) ODRMeasure *width;
@property(nonatomic, readonly) ODRMeasure *height;
@property(nonatomic, readonly, nullable) ODRDrawingTransform *transform;
/// `nil` for a shape whose geometry we cannot read, leaving its box.
@property(nonatomic, readonly, nullable) ODRDrawingPath *path;
@property(nonatomic, readonly) ODRGraphicStyle *style;
@end

Expand Down
27 changes: 27 additions & 0 deletions apple/src/ODRDocumentElement.mm
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,24 @@ - (ODRGraphicStyle *)style {

@end

@implementation ODRDrawingPath

+ (nullable instancetype)pathWithHandle:
(const std::optional<odr::DrawingPath> &)handle {
if (!handle.has_value()) {
return nil;
}
ODRDrawingPath *const result = [[ODRDrawingPath alloc] init];
result->_data = to_nsstring(handle->data);
result->_x = handle->x;
result->_y = handle->y;
result->_width = handle->width;
result->_height = handle->height;
return result;
}

@end

@implementation ODRDrawingTransform

+ (nullable instancetype)transformWithHandle:
Expand Down Expand Up @@ -958,6 +976,15 @@ - (nullable ODRDrawingTransform *)transform {
nil);
}

- (nullable ODRDrawingPath *)path {
return guarded_value(
[&]() -> ODRDrawingPath * {
return [ODRDrawingPath
pathWithHandle:self.handle.as_custom_shape().path()];
},
nil);
}

- (ODRGraphicStyle *)style {
return guarded_value(
[&]() -> ODRGraphicStyle * {
Expand Down
5 changes: 5 additions & 0 deletions apple/src/ODRPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ NS_ASSUME_NONNULL_BEGIN
+ (instancetype)styleWithHandle:(const odr::TableCellStyle &)handle;
@end

@interface ODRDrawingPath (Private)
+ (nullable instancetype)pathWithHandle:
(const std::optional<odr::DrawingPath> &)handle;
@end

@interface ODRDrawingTransform (Private)
+ (nullable instancetype)transformWithHandle:
(const std::optional<odr::DrawingTransform> &)handle;
Expand Down
1 change: 1 addition & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ add_jar(odr_java
"java/app/opendocument/core/DocumentFile.java"
"java/app/opendocument/core/DocumentPath.java"
"java/app/opendocument/core/DocumentType.java"
"java/app/opendocument/core/DrawingPath.java"
"java/app/opendocument/core/DrawingTransform.java"
"java/app/opendocument/core/Element.java"
"java/app/opendocument/core/ElementType.java"
Expand Down
6 changes: 6 additions & 0 deletions jni/java/app/opendocument/core/CustomShape.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public DrawingTransform transform() {
return transformNative(handle());
}

public DrawingPath path() {
return pathNative(handle());
}

public GraphicStyle style() {
return styleNative(handle());
}
Expand All @@ -40,5 +44,7 @@ public GraphicStyle style() {

private native DrawingTransform transformNative(long handle);

private native DrawingPath pathNative(long handle);

private native GraphicStyle styleNative(long handle);
}
40 changes: 40 additions & 0 deletions jni/java/app/opendocument/core/DrawingPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app.opendocument.core;

import java.util.Objects;

/** A drawing shape's outline, as an svg path. Mirrors {@code odr::DrawingPath}. */
public final class DrawingPath {
public final String data;
public final double x;
public final double y;
public final double width;
public final double height;

public DrawingPath(String data, double x, double y, double width, double height) {
this.data = Objects.requireNonNull(data);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}

@Override
public boolean equals(Object other) {
return other instanceof DrawingPath path
&& data.equals(path.data)
&& x == path.x
&& y == path.y
&& width == path.width
&& height == path.height;
}

@Override
public int hashCode() {
return Objects.hash(data, x, y, width, height);
}

@Override
public String toString() {
return data;
}
}
2 changes: 2 additions & 0 deletions jni/src/jni_convert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobject make_graphic_style(JNIEnv *env, const odr::GraphicStyle &style);
jobject
make_drawing_transform(JNIEnv *env,
const std::optional<odr::DrawingTransform> &transform);
jobject make_drawing_path(JNIEnv *env,
const std::optional<odr::DrawingPath> &path);
jobject make_page_layout(JNIEnv *env, const odr::PageLayout &layout);
jobject make_table_dimensions(JNIEnv *env,
const odr::TableDimensions &dimensions);
Expand Down
9 changes: 9 additions & 0 deletions jni/src/jni_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,15 @@ Java_app_opendocument_core_CustomShape_transformNative(JNIEnv *env, jobject,
});
}

extern "C" JNIEXPORT jobject JNICALL
Java_app_opendocument_core_CustomShape_pathNative(JNIEnv *env, jobject,
jlong handle) {
return guarded(env, [&] {
return odr_jni::make_drawing_path(env,
element(handle).as_custom_shape().path());
});
}

extern "C" JNIEXPORT jobject JNICALL
Java_app_opendocument_core_CustomShape_styleNative(JNIEnv *env, jobject,
jlong handle) {
Expand Down
10 changes: 10 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ make_drawing_transform(JNIEnv *env,
make_measure(env, transform->f));
}

jobject make_drawing_path(JNIEnv *env,
const std::optional<odr::DrawingPath> &path) {
if (!path.has_value()) {
return nullptr;
}
return new_object(env, "app/opendocument/core/DrawingPath",
"(Ljava/lang/String;DDDD)V", to_jstring(env, path->data),
path->x, path->y, path->width, path->height);
}

jobject make_color(JNIEnv *env, const std::optional<odr::Color> &value) {
if (!value.has_value()) {
return nullptr;
Expand Down
9 changes: 9 additions & 0 deletions python/src/bind_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ void odr_python::bind_document(py::module_ &m) {
.def("value_type", &odr::TableCell::value_type)
.def("style", &odr::TableCell::style);

py::class_<odr::DrawingPath>(m, "DrawingPath")
.def(py::init<>())
.def_readwrite("data", &odr::DrawingPath::data)
.def_readwrite("x", &odr::DrawingPath::x)
.def_readwrite("y", &odr::DrawingPath::y)
.def_readwrite("width", &odr::DrawingPath::width)
.def_readwrite("height", &odr::DrawingPath::height);

py::class_<odr::DrawingTransform>(m, "DrawingTransform")
.def(py::init<>())
.def_readwrite("a", &odr::DrawingTransform::a)
Expand Down Expand Up @@ -326,6 +334,7 @@ void odr_python::bind_document(py::module_ &m) {
.def("width", &odr::CustomShape::width)
.def("height", &odr::CustomShape::height)
.def("transform", &odr::CustomShape::transform)
.def("path", &odr::CustomShape::path)
.def("style", &odr::CustomShape::style);

bind_element<odr::Image>(m, "Image")
Expand Down
5 changes: 5 additions & 0 deletions src/odr/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@ std::optional<DrawingTransform> CustomShape::transform() const {
: std::optional<DrawingTransform>();
}

std::optional<DrawingPath> CustomShape::path() const {
return exists_() ? m_adapter2->custom_shape_path(m_identifier)
: std::optional<DrawingPath>();
}

GraphicStyle CustomShape::style() const {
return exists_() ? m_adapter2->custom_shape_style(m_identifier)
: GraphicStyle();
Expand Down
14 changes: 14 additions & 0 deletions src/odr/document_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,18 @@ class TableCell final
[[nodiscard]] TableCellStyle style() const;
};

/// @brief Represents a drawing shape's outline, as an svg path.
///
/// `data` is written in the user-space box `x`, `y`, `width`, `height`, which
/// the shape's own box stretches to, aspect ratio not preserved.
struct DrawingPath final {
std::string data;
double x{0};
double y{0};
double width{0};
double height{0};
};

/// @brief Represents the affine transform a drawing shape carries.
///
/// `(x, y)` maps to `(a*x + c*y + e, b*x + d*y + f)`, the lettering of
Expand Down Expand Up @@ -565,6 +577,8 @@ class CustomShape final
[[nodiscard]] Measure width() const;
[[nodiscard]] Measure height() const;
[[nodiscard]] std::optional<DrawingTransform> transform() const;
/// Nothing for a shape whose geometry we cannot read, leaving its box.
[[nodiscard]] std::optional<DrawingPath> path() const;

[[nodiscard]] GraphicStyle style() const;
};
Expand Down
3 changes: 3 additions & 0 deletions src/odr/internal/abstract/document.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct TextStyle;
struct ParagraphStyle;
struct GraphicStyle;
struct DrawingTransform;
struct DrawingPath;
} // namespace odr

namespace odr::internal::abstract {
Expand Down Expand Up @@ -515,6 +516,8 @@ class CustomShapeAdapter {
custom_shape_height(ElementIdentifier element_id) const = 0;
[[nodiscard]] virtual std::optional<DrawingTransform>
custom_shape_transform(ElementIdentifier element_id) const = 0;
[[nodiscard]] virtual std::optional<DrawingPath>
custom_shape_path(ElementIdentifier element_id) const = 0;

[[nodiscard]] virtual GraphicStyle
custom_shape_style(ElementIdentifier element_id) const = 0;
Expand Down
57 changes: 55 additions & 2 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
#include <odr/internal/html/html_service.hpp>
#include <odr/internal/html/html_writer.hpp>
#include <odr/internal/html/image_file.hpp>
#include <odr/internal/util/number_util.hpp>
#include <odr/internal/xml/xml_util.hpp>

#include <algorithm>

namespace odr::internal {

void html::translate_children(const ElementRange &range,
Expand Down Expand Up @@ -635,6 +638,21 @@ void html::translate_line(const Element &element, const WritingState &state) {
{"y2", line.y2().to_string()}}));

state.out().write_element_end("svg");

// A line's own text sits at its middle; most carry an empty paragraph and
// want no box at all.
if (std::ranges::any_of(line.children(), [](const Element &child) {
return has_content(child.children());
})) {
const std::string middle =
"position:absolute;left:calc((" + line.x1().to_string() + " + " +
line.x2().to_string() + ")/2);top:calc((" + line.y1().to_string() +
" + " + line.y2().to_string() + ")/2);transform:translate(-50%,-100%);";
state.out().write_element_begin("div",
HtmlElementOptions().set_style(middle));
translate_children(line.children(), state);
state.out().write_element_end("div");
}
}

void html::translate_circle(const Element &element, const WritingState &state) {
Expand All @@ -648,7 +666,7 @@ void html::translate_circle(const Element &element, const WritingState &state) {
state.out().write_new_line();
translate_children(circle.children(), state);
state.out().write_raw(
R"(<svg xmlns="http://www.w3.org/2000/svg" version="1.1" overflow="visible" preserveAspectRatio="none" style="z-index:-1;width:inherit;height:inherit;position:absolute;top:0;left:0;padding:inherit;"><circle cx="50%" cy="50%" r="50%" /></svg>)");
R"(<svg xmlns="http://www.w3.org/2000/svg" version="1.1" overflow="visible" preserveAspectRatio="none" style="z-index:-1;width:inherit;height:inherit;position:absolute;top:0;left:0;padding:inherit;"><ellipse cx="50%" cy="50%" rx="50%" ry="50%" /></svg>)");
state.out().write_element_end("div");
}

Expand All @@ -662,7 +680,42 @@ void html::translate_custom_shape(const Element &element,
translate_custom_shape_properties(custom_shape) +
translate_drawing_style(style)));
translate_children(custom_shape.children(), state);
// TODO draw shape in svg

if (const std::optional<DrawingPath> path = custom_shape.path();
path.has_value()) {
const auto number = [](const double value) {
return util::number::to_string_significant(value, 7);
};
state.out().write_new_line();
state.out().write_element_begin(
"svg",
HtmlElementOptions()
.set_attributes(HtmlAttributesVector{
{"xmlns", "http://www.w3.org/2000/svg"},
{"version", "1.1"},
{"overflow", "visible"},
{"preserveAspectRatio", "none"},
{"viewBox", number(path->x) + " " + number(path->y) + " " +
number(path->width) + " " +
number(path->height)}})
.set_style("z-index:-1;width:inherit;height:inherit;position:"
"absolute;top:0;left:0;padding:inherit;"));
HtmlAttributesVector attributes{
{"d", path->data},
// 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
// it did.
if (path->data.find_first_of("Zz") == std::string::npos) {
attributes.emplace_back("fill", "none");
}
state.out().write_element_begin("path",
HtmlElementOptions()
.set_close_type(HtmlCloseType::trailing)
.set_attributes(std::move(attributes)));
state.out().write_element_end("svg");
}

state.out().write_element_end("div");
}

Expand Down
Loading
Loading