diff --git a/AGENTS.md b/AGENTS.md index dead739d4..73a5c4601 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,12 @@ its own format has. Compact example: `rtf/rtf_element_registry.*` + `ElementType` is the shared enum in `src/odr/document_element.hpp`. +**Every drawing is a `frame`**, and `Frame::shape_type` says which outline it +draws — `none` for a plain box, else `rect`/`ellipse`/`line`/`custom`, the last +carrying its own `Frame::path`. Only odf sets anything but `none`; the other +engines funnel every shape into a plain frame, so `FrameAdapter` defaults the +three shape readers. + A **manual page break** reaches the renderer two ways, as the formats state it two ways: `ParagraphStyle::break_before`/`break_after` for odf and ooxml, where it is a style property, and `ElementType::page_break` for rtf and `.doc`, where diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3e37533..f8f9a1e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- **Breaking**: the drawing elements `Rect`, `Line`, `Circle` and `CustomShape` + are gone, with their `ElementType` values and `Element::as_rect`/`as_line`/ + `as_circle`/`as_custom_shape`. Every shape is a `Frame` now, naming itself + through the new `ShapeType Frame::shape_type()` and carrying `Frame::path()` + and the new `Frame::line()`. Shapes gain `anchor_type()` and `z_index()`, + which only frames had. Mirrored in the JNI, Apple and Python bindings. No + rendered html changes. Closes #773. + ## v6.14.0 - 2026-09-06 - Fixed: a semi-transparent colour wrote `rgba(0,0,0,0,501961)` where the host diff --git a/apple/include/OdrCoreObjC/ODRDocumentElement.h b/apple/include/OdrCoreObjC/ODRDocumentElement.h index 44c87f190..0299eba03 100644 --- a/apple/include/OdrCoreObjC/ODRDocumentElement.h +++ b/apple/include/OdrCoreObjC/ODRDocumentElement.h @@ -42,14 +42,17 @@ typedef NS_ENUM(NSInteger, ODRElementType) { ODRElementTypeFrame, ODRElementTypeImage, - ODRElementTypeRect, - ODRElementTypeLine, - ODRElementTypeCircle, - ODRElementTypeCustomShape, - ODRElementTypeGroup, } NS_SWIFT_NAME(ElementType); +typedef NS_ENUM(NSInteger, ODRShapeType) { + ODRShapeTypeNone = 0, + ODRShapeTypeRect, + ODRShapeTypeEllipse, + ODRShapeTypeLine, + ODRShapeTypeCustom, +} NS_SWIFT_NAME(ShapeType); + typedef NS_ENUM(NSInteger, ODRAnchorType) { ODRAnchorTypeAsChar = 0, ODRAnchorTypeAtChar, @@ -265,6 +268,18 @@ NS_SWIFT_NAME(DrawingPath) + (instancetype)new NS_UNAVAILABLE; @end +/// `odr::DrawingLine`: the two ends of a line shape, in the parent's space. +NS_SWIFT_NAME(DrawingLine) +@interface ODRDrawingLine : NSObject +@property(nonatomic, readonly) ODRMeasure *x1; +@property(nonatomic, readonly) ODRMeasure *y1; +@property(nonatomic, readonly) ODRMeasure *x2; +@property(nonatomic, readonly) ODRMeasure *y2; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; +@end + /// `odr::DrawingTransform`. NS_SWIFT_NAME(DrawingTransform) @interface ODRDrawingTransform : NSObject @@ -282,6 +297,7 @@ NS_SWIFT_NAME(DrawingTransform) /// `odr::Frame`. NS_SWIFT_NAME(Frame) @interface ODRFrame : ODRElement +@property(nonatomic, readonly) ODRShapeType shapeType; @property(nonatomic, readonly) ODRAnchorType anchorType; @property(nonatomic, readonly, nullable) ODRMeasure *x; @property(nonatomic, readonly, nullable) ODRMeasure *y; @@ -290,52 +306,10 @@ NS_SWIFT_NAME(Frame) /// `int32_t`, boxed; `nil` when the document did not set one. @property(nonatomic, readonly, nullable) NSNumber *zIndex; @property(nonatomic, readonly, nullable) ODRDrawingTransform *transform; -@property(nonatomic, readonly) ODRGraphicStyle *style; -@end - -/// `odr::Rect`. -NS_SWIFT_NAME(Rect) -@interface ODRRect : ODRElement -@property(nonatomic, readonly) ODRMeasure *x; -@property(nonatomic, readonly) ODRMeasure *y; -@property(nonatomic, readonly) ODRMeasure *width; -@property(nonatomic, readonly) ODRMeasure *height; -@property(nonatomic, readonly, nullable) ODRDrawingTransform *transform; -@property(nonatomic, readonly) ODRGraphicStyle *style; -@end - -/// `odr::Line`. -NS_SWIFT_NAME(Line) -@interface ODRLine : ODRElement -@property(nonatomic, readonly) ODRMeasure *x1; -@property(nonatomic, readonly) ODRMeasure *y1; -@property(nonatomic, readonly) ODRMeasure *x2; -@property(nonatomic, readonly) ODRMeasure *y2; -@property(nonatomic, readonly, nullable) ODRDrawingTransform *transform; -@property(nonatomic, readonly) ODRGraphicStyle *style; -@end - -/// `odr::Circle`. -NS_SWIFT_NAME(Circle) -@interface ODRCircle : ODRElement -@property(nonatomic, readonly) ODRMeasure *x; -@property(nonatomic, readonly) ODRMeasure *y; -@property(nonatomic, readonly) ODRMeasure *width; -@property(nonatomic, readonly) ODRMeasure *height; -@property(nonatomic, readonly, nullable) ODRDrawingTransform *transform; -@property(nonatomic, readonly) ODRGraphicStyle *style; -@end - -/// `odr::CustomShape`. -NS_SWIFT_NAME(CustomShape) -@interface ODRCustomShape : ODRElement -@property(nonatomic, readonly, nullable) ODRMeasure *x; -@property(nonatomic, readonly, nullable) ODRMeasure *y; -@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; +/// The ends of an `ODRShapeTypeLine`, which states them instead of a box. +@property(nonatomic, readonly, nullable) ODRDrawingLine *line; @property(nonatomic, readonly) ODRGraphicStyle *style; @end diff --git a/apple/src/ODRDocumentElement.mm b/apple/src/ODRDocumentElement.mm index b3e1e42a3..aab40313a 100644 --- a/apple/src/ODRDocumentElement.mm +++ b/apple/src/ODRDocumentElement.mm @@ -41,12 +41,14 @@ ODR_SAME_ENUM(ODRElementTypeTableCell, odr::ElementType::table_cell); ODR_SAME_ENUM(ODRElementTypeFrame, odr::ElementType::frame); ODR_SAME_ENUM(ODRElementTypeImage, odr::ElementType::image); -ODR_SAME_ENUM(ODRElementTypeRect, odr::ElementType::rect); -ODR_SAME_ENUM(ODRElementTypeLine, odr::ElementType::line); -ODR_SAME_ENUM(ODRElementTypeCircle, odr::ElementType::circle); -ODR_SAME_ENUM(ODRElementTypeCustomShape, odr::ElementType::custom_shape); ODR_SAME_ENUM(ODRElementTypeGroup, odr::ElementType::group); +ODR_SAME_ENUM(ODRShapeTypeNone, odr::ShapeType::none); +ODR_SAME_ENUM(ODRShapeTypeRect, odr::ShapeType::rect); +ODR_SAME_ENUM(ODRShapeTypeEllipse, odr::ShapeType::ellipse); +ODR_SAME_ENUM(ODRShapeTypeLine, odr::ShapeType::line); +ODR_SAME_ENUM(ODRShapeTypeCustom, odr::ShapeType::custom); + ODR_SAME_ENUM(ODRAnchorTypeAsChar, odr::AnchorType::as_char); ODR_SAME_ENUM(ODRAnchorTypeAtChar, odr::AnchorType::at_char); ODR_SAME_ENUM(ODRAnchorTypeAtFrame, odr::AnchorType::at_frame); @@ -154,18 +156,6 @@ + (nullable ODRElement *)elementWithHandle:(odr::Element)handle case odr::ElementType::image: klass = [ODRImage class]; break; - case odr::ElementType::rect: - klass = [ODRRect class]; - break; - case odr::ElementType::line: - klass = [ODRLine class]; - break; - case odr::ElementType::circle: - klass = [ODRCircle class]; - break; - case odr::ElementType::custom_shape: - klass = [ODRCustomShape class]; - break; // page_break, list and group have no typed C++ view of their own default: break; @@ -707,6 +697,14 @@ - (ODRTableCellStyle *)style { @implementation ODRFrame +- (ODRShapeType)shapeType { + return guarded_value( + [&] { + return static_cast(self.handle.as_frame().shape_type()); + }, + ODRShapeTypeNone); +} + - (ODRAnchorType)anchorType { return guarded_value( [&] { @@ -751,6 +749,22 @@ - (nullable ODRDrawingTransform *)transform { nil); } +- (nullable ODRDrawingPath *)path { + return guarded_value( + [&]() -> ODRDrawingPath * { + return [ODRDrawingPath pathWithHandle:self.handle.as_frame().path()]; + }, + nil); +} + +- (nullable ODRDrawingLine *)line { + return guarded_value( + [&]() -> ODRDrawingLine * { + return [ODRDrawingLine lineWithHandle:self.handle.as_frame().line()]; + }, + nil); +} + - (ODRGraphicStyle *)style { return guarded_value( [&]() -> ODRGraphicStyle * { @@ -779,6 +793,23 @@ + (nullable instancetype)pathWithHandle: @end +@implementation ODRDrawingLine + ++ (nullable instancetype)lineWithHandle: + (const std::optional &)handle { + if (!handle.has_value()) { + return nil; + } + ODRDrawingLine *const result = [[ODRDrawingLine alloc] init]; + result->_x1 = [ODRMeasure measureWithHandle:handle->x1]; + result->_y1 = [ODRMeasure measureWithHandle:handle->y1]; + result->_x2 = [ODRMeasure measureWithHandle:handle->x2]; + result->_y2 = [ODRMeasure measureWithHandle:handle->y2]; + return result; +} + +@end + @implementation ODRDrawingTransform + (nullable instancetype)transformWithHandle: @@ -798,213 +829,6 @@ + (nullable instancetype)transformWithHandle: @end -@implementation ODRRect - -- (ODRMeasure *)x { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_rect().x()]; }, - nil); -} - -- (ODRMeasure *)y { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_rect().y()]; }, - nil); -} - -- (ODRMeasure *)width { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_rect().width()]; - }, - nil); -} - -- (ODRMeasure *)height { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_rect().height()]; - }, - nil); -} - -- (nullable ODRDrawingTransform *)transform { - return guarded_value( - [&]() -> ODRDrawingTransform * { - return [ODRDrawingTransform - transformWithHandle:self.handle.as_rect().transform()]; - }, - nil); -} - -- (ODRGraphicStyle *)style { - return guarded_value( - [&]() -> ODRGraphicStyle * { - return [ODRGraphicStyle styleWithHandle:self.handle.as_rect().style()]; - }, - nil); -} - -@end - -@implementation ODRLine - -- (ODRMeasure *)x1 { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_line().x1()]; }, - nil); -} - -- (ODRMeasure *)y1 { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_line().y1()]; }, - nil); -} - -- (ODRMeasure *)x2 { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_line().x2()]; }, - nil); -} - -- (ODRMeasure *)y2 { - return guarded_value( - [&] { return [ODRMeasure measureWithHandle:self.handle.as_line().y2()]; }, - nil); -} - -- (nullable ODRDrawingTransform *)transform { - return guarded_value( - [&]() -> ODRDrawingTransform * { - return [ODRDrawingTransform - transformWithHandle:self.handle.as_line().transform()]; - }, - nil); -} - -- (ODRGraphicStyle *)style { - return guarded_value( - [&]() -> ODRGraphicStyle * { - return [ODRGraphicStyle styleWithHandle:self.handle.as_line().style()]; - }, - nil); -} - -@end - -@implementation ODRCircle - -- (ODRMeasure *)x { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_circle().x()]; - }, - nil); -} - -- (ODRMeasure *)y { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_circle().y()]; - }, - nil); -} - -- (ODRMeasure *)width { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_circle().width()]; - }, - nil); -} - -- (ODRMeasure *)height { - return guarded_value( - [&] { - return [ODRMeasure measureWithHandle:self.handle.as_circle().height()]; - }, - nil); -} - -- (nullable ODRDrawingTransform *)transform { - return guarded_value( - [&]() -> ODRDrawingTransform * { - return [ODRDrawingTransform - transformWithHandle:self.handle.as_circle().transform()]; - }, - nil); -} - -- (ODRGraphicStyle *)style { - return guarded_value( - [&]() -> ODRGraphicStyle * { - return - [ODRGraphicStyle styleWithHandle:self.handle.as_circle().style()]; - }, - nil); -} - -@end - -@implementation ODRCustomShape - -- (nullable ODRMeasure *)x { - return guarded_value([&] { return box(self.handle.as_custom_shape().x()); }, - nil); -} - -- (nullable ODRMeasure *)y { - return guarded_value([&] { return box(self.handle.as_custom_shape().y()); }, - nil); -} - -- (ODRMeasure *)width { - return guarded_value( - [&] { - return [ODRMeasure - measureWithHandle:self.handle.as_custom_shape().width()]; - }, - nil); -} - -- (ODRMeasure *)height { - return guarded_value( - [&] { - return [ODRMeasure - measureWithHandle:self.handle.as_custom_shape().height()]; - }, - nil); -} - -- (nullable ODRDrawingTransform *)transform { - return guarded_value( - [&]() -> ODRDrawingTransform * { - return [ODRDrawingTransform - transformWithHandle:self.handle.as_custom_shape().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 * { - return [ODRGraphicStyle - styleWithHandle:self.handle.as_custom_shape().style()]; - }, - nil); -} - -@end - @implementation ODRImage - (BOOL)isInternal { diff --git a/apple/src/ODRPrivate.h b/apple/src/ODRPrivate.h index 6013e113a..0fda18713 100644 --- a/apple/src/ODRPrivate.h +++ b/apple/src/ODRPrivate.h @@ -130,6 +130,11 @@ NS_ASSUME_NONNULL_BEGIN (const std::optional &)handle; @end +@interface ODRDrawingLine (Private) ++ (nullable instancetype)lineWithHandle: + (const std::optional &)handle; +@end + @interface ODRDrawingTransform (Private) + (nullable instancetype)transformWithHandle: (const std::optional &)handle; diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt index a0b330638..f932c52cb 100644 --- a/jni/CMakeLists.txt +++ b/jni/CMakeLists.txt @@ -69,9 +69,7 @@ add_jar(odr_java "java/app/opendocument/core/ArchiveFile.java" "java/app/opendocument/core/Bookmark.java" "java/app/opendocument/core/BreakType.java" - "java/app/opendocument/core/Circle.java" "java/app/opendocument/core/Color.java" - "java/app/opendocument/core/CustomShape.java" "java/app/opendocument/core/DecodePreference.java" "java/app/opendocument/core/DecodedFile.java" "java/app/opendocument/core/DirectionalMeasure.java" @@ -80,6 +78,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/DrawingLine.java" "java/app/opendocument/core/DrawingPath.java" "java/app/opendocument/core/DrawingTransform.java" "java/app/opendocument/core/Element.java" @@ -121,7 +120,6 @@ add_jar(odr_java "java/app/opendocument/core/HttpServer.java" "java/app/opendocument/core/Image.java" "java/app/opendocument/core/ImageFile.java" - "java/app/opendocument/core/Line.java" "java/app/opendocument/core/LineBreak.java" "java/app/opendocument/core/Link.java" "java/app/opendocument/core/ListElement.java" @@ -140,7 +138,7 @@ add_jar(odr_java "java/app/opendocument/core/PdfFile.java" "java/app/opendocument/core/PdfTextMode.java" "java/app/opendocument/core/PrintOrientation.java" - "java/app/opendocument/core/Rect.java" + "java/app/opendocument/core/ShapeType.java" "java/app/opendocument/core/Sheet.java" "java/app/opendocument/core/SheetCell.java" "java/app/opendocument/core/Slide.java" diff --git a/jni/java/app/opendocument/core/Circle.java b/jni/java/app/opendocument/core/Circle.java deleted file mode 100644 index 037d451ed..000000000 --- a/jni/java/app/opendocument/core/Circle.java +++ /dev/null @@ -1,44 +0,0 @@ -package app.opendocument.core; - -/** Circle element. Mirrors {@code odr::Circle}. */ -public final class Circle extends Element { - Circle(long handle, Object owner) { - super(handle, owner); - } - - public Measure x() { - return xNative(handle()); - } - - public Measure y() { - return yNative(handle()); - } - - public Measure width() { - return widthNative(handle()); - } - - public Measure height() { - return heightNative(handle()); - } - - public DrawingTransform transform() { - return transformNative(handle()); - } - - public GraphicStyle style() { - return styleNative(handle()); - } - - private native Measure xNative(long handle); - - private native Measure yNative(long handle); - - private native Measure widthNative(long handle); - - private native Measure heightNative(long handle); - - private native DrawingTransform transformNative(long handle); - - private native GraphicStyle styleNative(long handle); -} diff --git a/jni/java/app/opendocument/core/CustomShape.java b/jni/java/app/opendocument/core/CustomShape.java deleted file mode 100644 index 2c33e944c..000000000 --- a/jni/java/app/opendocument/core/CustomShape.java +++ /dev/null @@ -1,50 +0,0 @@ -package app.opendocument.core; - -/** Custom shape element. Mirrors {@code odr::CustomShape}; x/y may be {@code null}. */ -public final class CustomShape extends Element { - CustomShape(long handle, Object owner) { - super(handle, owner); - } - - public Measure x() { - return xNative(handle()); - } - - public Measure y() { - return yNative(handle()); - } - - public Measure width() { - return widthNative(handle()); - } - - public Measure height() { - return heightNative(handle()); - } - - public DrawingTransform transform() { - return transformNative(handle()); - } - - public DrawingPath path() { - return pathNative(handle()); - } - - public GraphicStyle style() { - return styleNative(handle()); - } - - private native Measure xNative(long handle); - - private native Measure yNative(long handle); - - private native Measure widthNative(long handle); - - private native Measure heightNative(long handle); - - private native DrawingTransform transformNative(long handle); - - private native DrawingPath pathNative(long handle); - - private native GraphicStyle styleNative(long handle); -} diff --git a/jni/java/app/opendocument/core/DrawingLine.java b/jni/java/app/opendocument/core/DrawingLine.java new file mode 100644 index 000000000..242d5f436 --- /dev/null +++ b/jni/java/app/opendocument/core/DrawingLine.java @@ -0,0 +1,37 @@ +package app.opendocument.core; + +import java.util.Objects; + +/** The two ends of a line shape. Mirrors {@code odr::DrawingLine}. */ +public final class DrawingLine { + public final Measure x1; + public final Measure y1; + public final Measure x2; + public final Measure y2; + + public DrawingLine(Measure x1, Measure y1, Measure x2, Measure y2) { + this.x1 = Objects.requireNonNull(x1); + this.y1 = Objects.requireNonNull(y1); + this.x2 = Objects.requireNonNull(x2); + this.y2 = Objects.requireNonNull(y2); + } + + @Override + public boolean equals(Object other) { + return other instanceof DrawingLine line + && x1.equals(line.x1) + && y1.equals(line.y1) + && x2.equals(line.x2) + && y2.equals(line.y2); + } + + @Override + public int hashCode() { + return Objects.hash(x1, y1, x2, y2); + } + + @Override + public String toString() { + return "(" + x1 + ", " + y1 + ") - (" + x2 + ", " + y2 + ")"; + } +} diff --git a/jni/java/app/opendocument/core/Element.java b/jni/java/app/opendocument/core/Element.java index da368af6f..dc6d13c8f 100644 --- a/jni/java/app/opendocument/core/Element.java +++ b/jni/java/app/opendocument/core/Element.java @@ -171,26 +171,6 @@ public Frame asFrame() { return h == 0 ? null : new Frame(h, owner()); } - public Rect asRect() { - long h = asRectNative(handle()); - return h == 0 ? null : new Rect(h, owner()); - } - - public Line asLine() { - long h = asLineNative(handle()); - return h == 0 ? null : new Line(h, owner()); - } - - public Circle asCircle() { - long h = asCircleNative(handle()); - return h == 0 ? null : new Circle(h, owner()); - } - - public CustomShape asCustomShape() { - long h = asCustomShapeNative(handle()); - return h == 0 ? null : new CustomShape(h, owner()); - } - public Image asImage() { long h = asImageNative(handle()); return h == 0 ? null : new Image(h, owner()); @@ -270,13 +250,5 @@ final List wrapAll(long[] handles) { private native long asFrameNative(long handle); - private native long asRectNative(long handle); - - private native long asLineNative(long handle); - - private native long asCircleNative(long handle); - - private native long asCustomShapeNative(long handle); - private native long asImageNative(long handle); } diff --git a/jni/java/app/opendocument/core/ElementType.java b/jni/java/app/opendocument/core/ElementType.java index 92271649f..560df2ec0 100644 --- a/jni/java/app/opendocument/core/ElementType.java +++ b/jni/java/app/opendocument/core/ElementType.java @@ -2,10 +2,29 @@ /** Mirrors {@code odr::ElementType}; constant order must match the C++ declaration. */ public enum ElementType { - NONE, ROOT, SLIDE, SHEET, PAGE, MASTER_PAGE, SHEET_CELL, TEXT, LINE_BREAK, - PAGE_BREAK, PARAGRAPH, SPAN, LINK, BOOKMARK, LIST, LIST_ITEM, TABLE, - TABLE_COLUMN, TABLE_ROW, TABLE_CELL, FRAME, IMAGE, RECT, LINE, CIRCLE, - CUSTOM_SHAPE, GROUP; + NONE, + ROOT, + SLIDE, + SHEET, + PAGE, + MASTER_PAGE, + SHEET_CELL, + TEXT, + LINE_BREAK, + PAGE_BREAK, + PARAGRAPH, + SPAN, + LINK, + BOOKMARK, + LIST, + LIST_ITEM, + TABLE, + TABLE_COLUMN, + TABLE_ROW, + TABLE_CELL, + FRAME, + IMAGE, + GROUP; static ElementType fromNative(int code) { return code < 0 ? null : values()[code]; diff --git a/jni/java/app/opendocument/core/Frame.java b/jni/java/app/opendocument/core/Frame.java index dabcaefac..3e1e41794 100644 --- a/jni/java/app/opendocument/core/Frame.java +++ b/jni/java/app/opendocument/core/Frame.java @@ -6,6 +6,10 @@ public final class Frame extends Element { super(handle, owner); } + public ShapeType shapeType() { + return ShapeType.fromNative(shapeTypeNative(handle())); + } + public AnchorType anchorType() { return AnchorType.fromNative(anchorTypeNative(handle())); } @@ -34,10 +38,22 @@ public DrawingTransform transform() { return transformNative(handle()); } + /** {@code null} for a shape whose geometry we cannot read, leaving its box. */ + public DrawingPath path() { + return pathNative(handle()); + } + + /** The ends of a {@link ShapeType#LINE}, which states them instead of a box. */ + public DrawingLine line() { + return lineNative(handle()); + } + public GraphicStyle style() { return styleNative(handle()); } + private native int shapeTypeNative(long handle); + private native int anchorTypeNative(long handle); private native Measure xNative(long handle); @@ -52,5 +68,9 @@ public GraphicStyle style() { private native DrawingTransform transformNative(long handle); + private native DrawingPath pathNative(long handle); + + private native DrawingLine lineNative(long handle); + private native GraphicStyle styleNative(long handle); } diff --git a/jni/java/app/opendocument/core/Line.java b/jni/java/app/opendocument/core/Line.java deleted file mode 100644 index ff20ab3c8..000000000 --- a/jni/java/app/opendocument/core/Line.java +++ /dev/null @@ -1,44 +0,0 @@ -package app.opendocument.core; - -/** Line element. Mirrors {@code odr::Line}. */ -public final class Line extends Element { - Line(long handle, Object owner) { - super(handle, owner); - } - - public Measure x1() { - return x1Native(handle()); - } - - public Measure y1() { - return y1Native(handle()); - } - - public Measure x2() { - return x2Native(handle()); - } - - public Measure y2() { - return y2Native(handle()); - } - - public DrawingTransform transform() { - return transformNative(handle()); - } - - public GraphicStyle style() { - return styleNative(handle()); - } - - private native Measure x1Native(long handle); - - private native Measure y1Native(long handle); - - private native Measure x2Native(long handle); - - private native Measure y2Native(long handle); - - private native DrawingTransform transformNative(long handle); - - private native GraphicStyle styleNative(long handle); -} diff --git a/jni/java/app/opendocument/core/Rect.java b/jni/java/app/opendocument/core/Rect.java deleted file mode 100644 index 71dd4aa11..000000000 --- a/jni/java/app/opendocument/core/Rect.java +++ /dev/null @@ -1,44 +0,0 @@ -package app.opendocument.core; - -/** Rectangle element. Mirrors {@code odr::Rect}. */ -public final class Rect extends Element { - Rect(long handle, Object owner) { - super(handle, owner); - } - - public Measure x() { - return xNative(handle()); - } - - public Measure y() { - return yNative(handle()); - } - - public Measure width() { - return widthNative(handle()); - } - - public Measure height() { - return heightNative(handle()); - } - - public DrawingTransform transform() { - return transformNative(handle()); - } - - public GraphicStyle style() { - return styleNative(handle()); - } - - private native Measure xNative(long handle); - - private native Measure yNative(long handle); - - private native Measure widthNative(long handle); - - private native Measure heightNative(long handle); - - private native DrawingTransform transformNative(long handle); - - private native GraphicStyle styleNative(long handle); -} diff --git a/jni/java/app/opendocument/core/ShapeType.java b/jni/java/app/opendocument/core/ShapeType.java new file mode 100644 index 000000000..733876922 --- /dev/null +++ b/jni/java/app/opendocument/core/ShapeType.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::ShapeType}; constant order must match the C++ declaration. */ +public enum ShapeType { + NONE, RECT, ELLIPSE, LINE, CUSTOM; + + static ShapeType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/src/jni_convert.hpp b/jni/src/jni_convert.hpp index bcbd783db..0d0b99588 100644 --- a/jni/src/jni_convert.hpp +++ b/jni/src/jni_convert.hpp @@ -40,6 +40,8 @@ make_drawing_transform(JNIEnv *env, const std::optional &transform); jobject make_drawing_path(JNIEnv *env, const std::optional &path); +jobject make_drawing_line(JNIEnv *env, + const std::optional &line); jobject make_page_layout(JNIEnv *env, const odr::PageLayout &layout); jobject make_table_dimensions(JNIEnv *env, const odr::TableDimensions &dimensions); diff --git a/jni/src/jni_document.cpp b/jni/src/jni_document.cpp index 22e35789d..5ed9266c1 100644 --- a/jni/src/jni_document.cpp +++ b/jni/src/jni_document.cpp @@ -333,10 +333,6 @@ ODR_JNI_ELEMENT_AS(TableColumn, as_table_column) ODR_JNI_ELEMENT_AS(TableRow, as_table_row) ODR_JNI_ELEMENT_AS(TableCell, as_table_cell) ODR_JNI_ELEMENT_AS(Frame, as_frame) -ODR_JNI_ELEMENT_AS(Rect, as_rect) -ODR_JNI_ELEMENT_AS(Line, as_line) -ODR_JNI_ELEMENT_AS(Circle, as_circle) -ODR_JNI_ELEMENT_AS(CustomShape, as_custom_shape) ODR_JNI_ELEMENT_AS(Image, as_image) #undef ODR_JNI_ELEMENT_AS @@ -793,6 +789,14 @@ Java_app_opendocument_core_TableCell_styleNative(JNIEnv *env, jobject, // app.opendocument.core.Frame +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Frame_shapeTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_frame().shape_type()); + }); +} + extern "C" JNIEXPORT jint JNICALL Java_app_opendocument_core_Frame_anchorTypeNative(JNIEnv *env, jobject, jlong handle) { @@ -849,219 +853,26 @@ Java_app_opendocument_core_Frame_transformNative(JNIEnv *env, jobject, } extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Frame_styleNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_graphic_style(env, element(handle).as_frame().style()); - }); -} - -// app.opendocument.core.Rect - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_xNative(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_rect().x()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_yNative(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_rect().y()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_widthNative(JNIEnv *env, jobject, +Java_app_opendocument_core_Frame_pathNative(JNIEnv *env, jobject, jlong handle) { return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_rect().width()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_heightNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_rect().height()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_transformNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_drawing_transform( - env, element(handle).as_rect().transform()); + return odr_jni::make_drawing_path(env, element(handle).as_frame().path()); }); } extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Rect_styleNative(JNIEnv *env, jobject, +Java_app_opendocument_core_Frame_lineNative(JNIEnv *env, jobject, jlong handle) { return guarded(env, [&] { - return odr_jni::make_graphic_style(env, element(handle).as_rect().style()); - }); -} - -// app.opendocument.core.Line - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_x1Native(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_line().x1()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_y1Native(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_line().y1()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_x2Native(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_line().x2()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_y2Native(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_line().y2()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_transformNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_drawing_transform( - env, element(handle).as_line().transform()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Line_styleNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_graphic_style(env, element(handle).as_line().style()); - }); -} - -// app.opendocument.core.Circle - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_xNative(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_circle().x()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_yNative(JNIEnv *env, jobject, jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_circle().y()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_widthNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_circle().width()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_heightNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_circle().height()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_transformNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_drawing_transform( - env, element(handle).as_circle().transform()); + return odr_jni::make_drawing_line(env, element(handle).as_frame().line()); }); } extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_Circle_styleNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_graphic_style(env, - element(handle).as_circle().style()); - }); -} - -// app.opendocument.core.CustomShape - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_CustomShape_xNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_custom_shape().x()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_CustomShape_yNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, element(handle).as_custom_shape().y()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_CustomShape_widthNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, - element(handle).as_custom_shape().width()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_CustomShape_heightNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_measure(env, - element(handle).as_custom_shape().height()); - }); -} - -extern "C" JNIEXPORT jobject JNICALL -Java_app_opendocument_core_CustomShape_transformNative(JNIEnv *env, jobject, - jlong handle) { - return guarded(env, [&] { - return odr_jni::make_drawing_transform( - env, element(handle).as_custom_shape().transform()); - }); -} - -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) { +Java_app_opendocument_core_Frame_styleNative(JNIEnv *env, jobject, + jlong handle) { return guarded(env, [&] { - return odr_jni::make_graphic_style( - env, element(handle).as_custom_shape().style()); + return odr_jni::make_graphic_style(env, element(handle).as_frame().style()); }); } diff --git a/jni/src/jni_style.cpp b/jni/src/jni_style.cpp index 49bba8d8c..62e7258f0 100644 --- a/jni/src/jni_style.cpp +++ b/jni/src/jni_style.cpp @@ -184,6 +184,19 @@ jobject make_drawing_path(JNIEnv *env, path->x, path->y, path->width, path->height); } +jobject make_drawing_line(JNIEnv *env, + const std::optional &line) { + if (!line.has_value()) { + return nullptr; + } + return new_object( + env, "app/opendocument/core/DrawingLine", + "(Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;" + "Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;)V", + make_measure(env, line->x1), make_measure(env, line->y1), + make_measure(env, line->x2), make_measure(env, line->y2)); +} + jobject make_color(JNIEnv *env, const std::optional &value) { if (!value.has_value()) { return nullptr; diff --git a/python/src/bind_document.cpp b/python/src/bind_document.cpp index d7da14191..761918149 100644 --- a/python/src/bind_document.cpp +++ b/python/src/bind_document.cpp @@ -35,10 +35,11 @@ py::object make_children_iterator(const odr::Element &element) { /// `__bool__` has to come from the derived type: `Element::operator bool` /// ignores the typed adapter, so a failed `as_*` cast would look valid. -template -py::class_ bind_element(py::module_ &m, const char *name) { - return py::class_(m, name).def("__bool__", - &T::operator bool); +template +py::class_ bind_element(py::module_ &m, const char *name, + const Extra &...extra) { + return py::class_(m, name, extra...) + .def("__bool__", &T::operator bool); } } // namespace @@ -67,12 +68,15 @@ void odr_python::bind_document(py::module_ &m) { .value("table_cell", odr::ElementType::table_cell) .value("frame", odr::ElementType::frame) .value("image", odr::ElementType::image) - .value("rect", odr::ElementType::rect) - .value("line", odr::ElementType::line) - .value("circle", odr::ElementType::circle) - .value("custom_shape", odr::ElementType::custom_shape) .value("group", odr::ElementType::group); + py::enum_(m, "ShapeType") + .value("none", odr::ShapeType::none) + .value("rect", odr::ShapeType::rect) + .value("ellipse", odr::ShapeType::ellipse) + .value("line", odr::ShapeType::line) + .value("custom", odr::ShapeType::custom); + py::enum_(m, "AnchorType") .value("as_char", odr::AnchorType::as_char) .value("at_char", odr::AnchorType::at_char) @@ -173,10 +177,6 @@ void odr_python::bind_document(py::module_ &m) { .def("as_table_row", &odr::Element::as_table_row, keep_self_alive) .def("as_table_cell", &odr::Element::as_table_cell, keep_self_alive) .def("as_frame", &odr::Element::as_frame, keep_self_alive) - .def("as_rect", &odr::Element::as_rect, keep_self_alive) - .def("as_line", &odr::Element::as_line, keep_self_alive) - .def("as_circle", &odr::Element::as_circle, keep_self_alive) - .def("as_custom_shape", &odr::Element::as_custom_shape, keep_self_alive) .def("as_image", &odr::Element::as_image, keep_self_alive); bind_element(m, "TextRoot") @@ -296,6 +296,7 @@ void odr_python::bind_document(py::module_ &m) { .def_readwrite("f", &odr::DrawingTransform::f); bind_element(m, "Frame") + .def("shape_type", &odr::Frame::shape_type) .def("anchor_type", &odr::Frame::anchor_type) .def("x", &odr::Frame::x) .def("y", &odr::Frame::y) @@ -303,41 +304,10 @@ void odr_python::bind_document(py::module_ &m) { .def("height", &odr::Frame::height) .def("z_index", &odr::Frame::z_index) .def("transform", &odr::Frame::transform) + .def("path", &odr::Frame::path) + .def("line", &odr::Frame::line) .def("style", &odr::Frame::style); - bind_element(m, "Rect") - .def("x", &odr::Rect::x) - .def("y", &odr::Rect::y) - .def("width", &odr::Rect::width) - .def("height", &odr::Rect::height) - .def("transform", &odr::Rect::transform) - .def("style", &odr::Rect::style); - - bind_element(m, "Line") - .def("x1", &odr::Line::x1) - .def("y1", &odr::Line::y1) - .def("x2", &odr::Line::x2) - .def("y2", &odr::Line::y2) - .def("transform", &odr::Line::transform) - .def("style", &odr::Line::style); - - bind_element(m, "Circle") - .def("x", &odr::Circle::x) - .def("y", &odr::Circle::y) - .def("width", &odr::Circle::width) - .def("height", &odr::Circle::height) - .def("transform", &odr::Circle::transform) - .def("style", &odr::Circle::style); - - bind_element(m, "CustomShape") - .def("x", &odr::CustomShape::x) - .def("y", &odr::CustomShape::y) - .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(m, "Image") .def("is_internal", &odr::Image::is_internal) .def("file", &odr::Image::file) diff --git a/src/odr/document_element.cpp b/src/odr/document_element.cpp index f41cb3be0..a3c657e79 100644 --- a/src/odr/document_element.cpp +++ b/src/odr/document_element.cpp @@ -208,35 +208,6 @@ Frame Element::as_frame() const { return {m_adapter, m_identifier, m_adapter->frame_adapter(m_identifier)}; } -Rect Element::as_rect() const { - if (!exists_()) { - return {}; - } - return {m_adapter, m_identifier, m_adapter->rect_adapter(m_identifier)}; -} - -Line Element::as_line() const { - if (!exists_()) { - return {}; - } - return {m_adapter, m_identifier, m_adapter->line_adapter(m_identifier)}; -} - -Circle Element::as_circle() const { - if (!exists_()) { - return {}; - } - return {m_adapter, m_identifier, m_adapter->circle_adapter(m_identifier)}; -} - -CustomShape Element::as_custom_shape() const { - if (!exists_()) { - return {}; - } - return {m_adapter, m_identifier, - m_adapter->custom_shape_adapter(m_identifier)}; -} - Image Element::as_image() const { if (!exists_()) { return {}; @@ -551,6 +522,11 @@ TableCellStyle TableCell::style() const { : TableCellStyle(); } +ShapeType Frame::shape_type() const { + return exists_() ? m_adapter2->frame_shape_type(m_identifier) + : ShapeType::none; +} + AnchorType Frame::anchor_type() const { return exists_() ? m_adapter2->frame_anchor_type(m_identifier) : AnchorType::as_char; // TODO default? @@ -586,118 +562,18 @@ std::optional Frame::transform() const { : std::optional(); } -GraphicStyle Frame::style() const { - return exists_() ? m_adapter2->frame_style(m_identifier) : GraphicStyle(); -} - -Measure Rect::x() const { - return exists_() ? m_adapter2->rect_x(m_identifier) : Measure(0, {}); -} - -Measure Rect::y() const { - return exists_() ? m_adapter2->rect_y(m_identifier) : Measure(0, {}); -} - -Measure Rect::width() const { - return exists_() ? m_adapter2->rect_width(m_identifier) : Measure(0, {}); -} - -Measure Rect::height() const { - return exists_() ? m_adapter2->rect_height(m_identifier) : Measure(0, {}); -} - -std::optional Rect::transform() const { - return exists_() ? m_adapter2->rect_transform(m_identifier) - : std::optional(); -} - -GraphicStyle Rect::style() const { - return exists_() ? m_adapter2->rect_style(m_identifier) : GraphicStyle(); -} - -Measure Line::x1() const { - return exists_() ? m_adapter2->line_x1(m_identifier) : Measure(0, {}); -} - -Measure Line::y1() const { - return exists_() ? m_adapter2->line_y1(m_identifier) : Measure(0, {}); -} - -Measure Line::x2() const { - return exists_() ? m_adapter2->line_x2(m_identifier) : Measure(0, {}); -} - -Measure Line::y2() const { - return exists_() ? m_adapter2->line_y2(m_identifier) : Measure(0, {}); -} - -std::optional Line::transform() const { - return exists_() ? m_adapter2->line_transform(m_identifier) - : std::optional(); -} - -GraphicStyle Line::style() const { - return exists_() ? m_adapter2->line_style(m_identifier) : GraphicStyle(); -} - -Measure Circle::x() const { - return exists_() ? m_adapter2->circle_x(m_identifier) : Measure(0, {}); -} - -Measure Circle::y() const { - return exists_() ? m_adapter2->circle_y(m_identifier) : Measure(0, {}); -} - -Measure Circle::width() const { - return exists_() ? m_adapter2->circle_width(m_identifier) : Measure(0, {}); -} - -Measure Circle::height() const { - return exists_() ? m_adapter2->circle_height(m_identifier) : Measure(0, {}); -} - -std::optional Circle::transform() const { - return exists_() ? m_adapter2->circle_transform(m_identifier) - : std::optional(); -} - -GraphicStyle Circle::style() const { - return exists_() ? m_adapter2->circle_style(m_identifier) : GraphicStyle(); -} - -std::optional CustomShape::x() const { - return exists_() ? m_adapter2->custom_shape_x(m_identifier) - : std::optional(); -} - -std::optional CustomShape::y() const { - return exists_() ? m_adapter2->custom_shape_y(m_identifier) - : std::optional(); -} - -Measure CustomShape::width() const { - return exists_() ? m_adapter2->custom_shape_width(m_identifier) - : Measure(0, {}); -} - -Measure CustomShape::height() const { - return exists_() ? m_adapter2->custom_shape_height(m_identifier) - : Measure(0, {}); -} - -std::optional CustomShape::transform() const { - return exists_() ? m_adapter2->custom_shape_transform(m_identifier) - : std::optional(); +std::optional Frame::path() const { + return exists_() ? m_adapter2->frame_path(m_identifier) + : std::optional(); } -std::optional CustomShape::path() const { - return exists_() ? m_adapter2->custom_shape_path(m_identifier) - : std::optional(); +std::optional Frame::line() const { + return exists_() ? m_adapter2->frame_line(m_identifier) + : std::optional(); } -GraphicStyle CustomShape::style() const { - return exists_() ? m_adapter2->custom_shape_style(m_identifier) - : GraphicStyle(); +GraphicStyle Frame::style() const { + return exists_() ? m_adapter2->frame_style(m_identifier) : GraphicStyle(); } bool Image::is_internal() const { diff --git a/src/odr/document_element.hpp b/src/odr/document_element.hpp index 5886ef044..bc478f499 100644 --- a/src/odr/document_element.hpp +++ b/src/odr/document_element.hpp @@ -48,10 +48,6 @@ class TableColumnAdapter; class TableRowAdapter; class TableCellAdapter; class FrameAdapter; -class RectAdapter; -class LineAdapter; -class CircleAdapter; -class CustomShapeAdapter; class ImageAdapter; } // namespace odr::internal::abstract @@ -79,10 +75,6 @@ class TableColumn; class TableRow; class TableCell; class Frame; -class Rect; -class Line; -class Circle; -class CustomShape; class Image; /// @brief Collection of element types. @@ -116,14 +108,19 @@ enum class ElementType { frame, image, - rect, - line, - circle, - custom_shape, group, }; +/// @brief Collection of shapes a frame draws. +enum class ShapeType { + none, ///< a plain box, drawing no outline of its own + rect, + ellipse, + line, + custom, ///< an outline of its own, read from @ref Frame::path +}; + /// @brief Collection of anchor types. enum class AnchorType { as_char, @@ -189,10 +186,6 @@ class Element { [[nodiscard]] TableRow as_table_row() const; [[nodiscard]] TableCell as_table_cell() const; [[nodiscard]] Frame as_frame() const; - [[nodiscard]] Rect as_rect() const; - [[nodiscard]] Line as_line() const; - [[nodiscard]] Circle as_circle() const; - [[nodiscard]] CustomShape as_custom_shape() const; [[nodiscard]] Image as_image() const; protected: @@ -497,6 +490,14 @@ struct DrawingPath final { double height{0}; }; +/// @brief Represents the two ends of a line shape, in the parent's space. +struct DrawingLine final { + Measure x1{0, DynamicUnit()}; + Measure y1{0, DynamicUnit()}; + Measure x2{0, DynamicUnit()}; + Measure y2{0, DynamicUnit()}; +}; + /// @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 @@ -516,6 +517,7 @@ class Frame final : public ElementBase { public: using ElementBase::ElementBase; + [[nodiscard]] ShapeType shape_type() const; [[nodiscard]] AnchorType anchor_type() const; [[nodiscard]] std::optional x() const; [[nodiscard]] std::optional y() const; @@ -523,65 +525,10 @@ class Frame final : public ElementBase { [[nodiscard]] std::optional height() const; [[nodiscard]] std::optional z_index() const; [[nodiscard]] std::optional transform() const; - - [[nodiscard]] GraphicStyle style() const; -}; - -/// @brief Represents a rectangle element in a document. -class Rect final : public ElementBase { -public: - using ElementBase::ElementBase; - - [[nodiscard]] Measure x() const; - [[nodiscard]] Measure y() const; - [[nodiscard]] Measure width() const; - [[nodiscard]] Measure height() const; - [[nodiscard]] std::optional transform() const; - - [[nodiscard]] GraphicStyle style() const; -}; - -/// @brief Represents a line element in a document. -class Line final : public ElementBase { -public: - using ElementBase::ElementBase; - - [[nodiscard]] Measure x1() const; - [[nodiscard]] Measure y1() const; - [[nodiscard]] Measure x2() const; - [[nodiscard]] Measure y2() const; - [[nodiscard]] std::optional transform() const; - - [[nodiscard]] GraphicStyle style() const; -}; - -/// @brief Represents a circle element in a document. -class Circle final : public ElementBase { -public: - using ElementBase::ElementBase; - - [[nodiscard]] Measure x() const; - [[nodiscard]] Measure y() const; - [[nodiscard]] Measure width() const; - [[nodiscard]] Measure height() const; - [[nodiscard]] std::optional transform() const; - - [[nodiscard]] GraphicStyle style() const; -}; - -/// @brief Represents a custom shape element in a document. -class CustomShape final - : public ElementBase { -public: - using ElementBase::ElementBase; - - [[nodiscard]] std::optional x() const; - [[nodiscard]] std::optional y() const; - [[nodiscard]] Measure width() const; - [[nodiscard]] Measure height() const; - [[nodiscard]] std::optional transform() const; /// Nothing for a shape whose geometry we cannot read, leaving its box. [[nodiscard]] std::optional path() const; + /// The ends of a @ref ShapeType::line, which states them instead of a box. + [[nodiscard]] std::optional line() const; [[nodiscard]] GraphicStyle style() const; }; diff --git a/src/odr/internal/abstract/document.hpp b/src/odr/internal/abstract/document.hpp index b43ccbc0d..e8740faf4 100644 --- a/src/odr/internal/abstract/document.hpp +++ b/src/odr/internal/abstract/document.hpp @@ -1,6 +1,9 @@ #pragma once #include +// for the element model itself: `FrameAdapter` defaults its shape readers, and +// a default needs the complete type. +#include #include #include @@ -13,11 +16,7 @@ namespace odr { class File; enum class FileType; enum class DocumentType; -enum class ElementType; class DocumentPath; -enum class ValueType; -enum class AnchorType; -enum class ListType; struct PageLayout; struct TableDimensions; struct TablePosition; @@ -28,8 +27,6 @@ struct TableCellStyle; struct TextStyle; struct ParagraphStyle; struct GraphicStyle; -struct DrawingTransform; -struct DrawingPath; } // namespace odr namespace odr::internal::abstract { @@ -54,10 +51,6 @@ class TableColumnAdapter; class TableRowAdapter; class TableCellAdapter; class FrameAdapter; -class RectAdapter; -class LineAdapter; -class CircleAdapter; -class CustomShapeAdapter; class ImageAdapter; class Document { @@ -190,22 +183,6 @@ class ElementAdapter { frame_adapter([[maybe_unused]] const ElementIdentifier element_id) const { return nullptr; } - [[nodiscard]] virtual const RectAdapter * - rect_adapter([[maybe_unused]] const ElementIdentifier element_id) const { - return nullptr; - } - [[nodiscard]] virtual const LineAdapter * - line_adapter([[maybe_unused]] const ElementIdentifier element_id) const { - return nullptr; - } - [[nodiscard]] virtual const CircleAdapter * - circle_adapter([[maybe_unused]] const ElementIdentifier element_id) const { - return nullptr; - } - [[nodiscard]] virtual const CustomShapeAdapter *custom_shape_adapter( - [[maybe_unused]] const ElementIdentifier element_id) const { - return nullptr; - } [[nodiscard]] virtual const ImageAdapter * image_adapter([[maybe_unused]] const ElementIdentifier element_id) const { return nullptr; @@ -437,6 +414,21 @@ class FrameAdapter { public: virtual ~FrameAdapter() = default; + /// The three shape readers default: an engine that only makes plain frames + /// says nothing. + [[nodiscard]] virtual ShapeType + frame_shape_type([[maybe_unused]] const ElementIdentifier element_id) const { + return ShapeType::none; + } + [[nodiscard]] virtual std::optional + frame_path([[maybe_unused]] const ElementIdentifier element_id) const { + return {}; + } + [[nodiscard]] virtual std::optional + frame_line([[maybe_unused]] const ElementIdentifier element_id) const { + return {}; + } + [[nodiscard]] virtual AnchorType frame_anchor_type(ElementIdentifier element_id) const = 0; [[nodiscard]] virtual std::optional @@ -456,78 +448,6 @@ class FrameAdapter { frame_style(ElementIdentifier element_id) const = 0; }; -class RectAdapter { -public: - virtual ~RectAdapter() = default; - - [[nodiscard]] virtual Measure rect_x(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure rect_y(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - rect_width(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - rect_height(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - rect_transform(ElementIdentifier element_id) const = 0; - - [[nodiscard]] virtual GraphicStyle - rect_style(ElementIdentifier element_id) const = 0; -}; - -class LineAdapter { -public: - virtual ~LineAdapter() = default; - - [[nodiscard]] virtual Measure line_x1(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure line_y1(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure line_x2(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure line_y2(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - line_transform(ElementIdentifier element_id) const = 0; - - [[nodiscard]] virtual GraphicStyle - line_style(ElementIdentifier element_id) const = 0; -}; - -class CircleAdapter { -public: - virtual ~CircleAdapter() = default; - - [[nodiscard]] virtual Measure - circle_x(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - circle_y(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - circle_width(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - circle_height(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - circle_transform(ElementIdentifier element_id) const = 0; - - [[nodiscard]] virtual GraphicStyle - circle_style(ElementIdentifier element_id) const = 0; -}; - -class CustomShapeAdapter { -public: - virtual ~CustomShapeAdapter() = default; - - [[nodiscard]] virtual std::optional - custom_shape_x(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - custom_shape_y(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - custom_shape_width(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual Measure - custom_shape_height(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - custom_shape_transform(ElementIdentifier element_id) const = 0; - [[nodiscard]] virtual std::optional - custom_shape_path(ElementIdentifier element_id) const = 0; - - [[nodiscard]] virtual GraphicStyle - custom_shape_style(ElementIdentifier element_id) const = 0; -}; - class ImageAdapter { public: virtual ~ImageAdapter() = default; diff --git a/src/odr/internal/common/element_adapter.hpp b/src/odr/internal/common/element_adapter.hpp index 624d31cff..26fd2de04 100644 --- a/src/odr/internal/common/element_adapter.hpp +++ b/src/odr/internal/common/element_adapter.hpp @@ -124,23 +124,6 @@ class ElementAdapter : public abstract::ElementAdapter, public Adapters... { frame_adapter(const ElementIdentifier element_id) const override { return adapter_(element_id); } - [[nodiscard]] const abstract::RectAdapter * - rect_adapter(const ElementIdentifier element_id) const override { - return adapter_(element_id); - } - [[nodiscard]] const abstract::LineAdapter * - line_adapter(const ElementIdentifier element_id) const override { - return adapter_(element_id); - } - [[nodiscard]] const abstract::CircleAdapter * - circle_adapter(const ElementIdentifier element_id) const override { - return adapter_(element_id); - } - [[nodiscard]] const abstract::CustomShapeAdapter * - custom_shape_adapter(const ElementIdentifier element_id) const override { - return adapter_( - element_id); - } [[nodiscard]] const abstract::ImageAdapter * image_adapter(const ElementIdentifier element_id) const override { return adapter_(element_id); diff --git a/src/odr/internal/html/document_element.cpp b/src/odr/internal/html/document_element.cpp index 196fd1544..4df5dcc2b 100644 --- a/src/odr/internal/html/document_element.cpp +++ b/src/odr/internal/html/document_element.cpp @@ -72,14 +72,6 @@ const char *element_type_name(const ElementType type) { return "frame"; case ElementType::image: return "image"; - case ElementType::rect: - return "rect"; - case ElementType::line: - return "line"; - case ElementType::circle: - return "circle"; - case ElementType::custom_shape: - return "custom_shape"; case ElementType::group: return "group"; } @@ -131,18 +123,6 @@ void html::translate_element(const Element &element, case ElementType::image: translate_image(element, state); break; - case ElementType::rect: - translate_rect(element, state); - break; - case ElementType::line: - translate_line(element, state); - break; - case ElementType::circle: - translate_circle(element, state); - break; - case ElementType::custom_shape: - translate_custom_shape(element, state); - break; case ElementType::page_break: translate_page_break(element, state); break; @@ -959,11 +939,12 @@ void html::translate_image(const Element &element, const WritingState &state) { .set_style("position:absolute;left:0;top:0;width:100%;height:100%")); } -void html::translate_frame(const Element &element, const WritingState &state) { - const Frame frame = element.as_frame(); - const GraphicStyle style = frame.style(); +namespace html { +namespace { - // A frame is a plain box, so its fill has to be a background - the `fill` +void translate_plain_frame(const Frame &frame, const GraphicStyle &style, + const WritingState &state) { + // A plain frame is a box, so its fill has to be a background - the `fill` // that `translate_drawing_style` writes only reaches the svg a shape carries. std::string background; if (style.fill_color.has_value() && style.fill_color->alpha != 0) { @@ -977,23 +958,33 @@ void html::translate_frame(const Element &element, const WritingState &state) { state.out().write_element_end("div"); } -void html::translate_rect(const Element &element, const WritingState &state) { - const Rect rect = element.as_rect(); - const GraphicStyle style = rect.style(); - +void translate_rect(const Frame &frame, const GraphicStyle &style, + const WritingState &state) { state.out().write_element_begin( - "div", HtmlElementOptions().set_style(translate_rect_properties(rect) + + "div", HtmlElementOptions().set_style(translate_shape_properties(frame) + translate_drawing_style(style))); - translate_children(rect.children(), state); + translate_children(frame.children(), state); state.out().write_new_line(); state.out().write_raw( R"()"); state.out().write_element_end("div"); } -void html::translate_line(const Element &element, const WritingState &state) { - const Line line = element.as_line(); - const GraphicStyle style = line.style(); +void translate_ellipse(const Frame &frame, const GraphicStyle &style, + const WritingState &state) { + state.out().write_element_begin( + "div", HtmlElementOptions().set_style(translate_shape_properties(frame) + + translate_drawing_style(style))); + state.out().write_new_line(); + translate_children(frame.children(), state); + state.out().write_raw( + R"()"); + state.out().write_element_end("div"); +} + +void translate_line(const Frame &frame, const GraphicStyle &style, + const WritingState &state) { + const DrawingLine line = frame.line().value_or(DrawingLine()); state.out().write_element_begin( "svg", HtmlElementOptions() @@ -1003,63 +994,43 @@ void html::translate_line(const Element &element, const WritingState &state) { {"overflow", "visible"}}) .set_style("z-index:-1;position:absolute;top:0;left:0;" + translate_drawing_style(style) + - translate_drawing_transform(line.transform()))); + translate_drawing_transform(frame.transform()))); state.out().write_element_begin( "line", HtmlElementOptions() .set_close_type(HtmlCloseType::trailing) - .set_attributes(HtmlAttributesVector{{"x1", line.x1().to_string()}, - {"y1", line.y1().to_string()}, - {"x2", line.x2().to_string()}, - {"y2", line.y2().to_string()}})); + .set_attributes(HtmlAttributesVector{{"x1", line.x1.to_string()}, + {"y1", line.y1.to_string()}, + {"x2", line.x2.to_string()}, + {"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) { + if (std::ranges::any_of(frame.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%);"; + "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); + translate_children(frame.children(), state); state.out().write_element_end("div"); } } -void html::translate_circle(const Element &element, const WritingState &state) { - const Circle circle = element.as_circle(); - const GraphicStyle style = circle.style(); - +void translate_custom_shape(const Frame &frame, const GraphicStyle &style, + const WritingState &state) { state.out().write_element_begin( - "div", - HtmlElementOptions().set_style(translate_circle_properties(circle) + - translate_drawing_style(style))); - state.out().write_new_line(); - translate_children(circle.children(), state); - state.out().write_raw( - R"()"); - state.out().write_element_end("div"); -} - -void html::translate_custom_shape(const Element &element, - const WritingState &state) { - const CustomShape custom_shape = element.as_custom_shape(); - const GraphicStyle style = custom_shape.style(); - - state.out().write_element_begin( - "div", HtmlElementOptions().set_style( - translate_custom_shape_properties(custom_shape) + - translate_drawing_style(style))); - translate_children(custom_shape.children(), state); + "div", HtmlElementOptions().set_style(translate_shape_properties(frame) + + translate_drawing_style(style))); + translate_children(frame.children(), state); - if (const std::optional path = custom_shape.path(); - path.has_value()) { + if (const std::optional path = frame.path(); path.has_value()) { const auto number = [](const double value) { return util::number::to_string_significant(value, 7); }; @@ -1098,4 +1069,30 @@ void html::translate_custom_shape(const Element &element, state.out().write_element_end("div"); } +} // namespace +} // namespace html + +void html::translate_frame(const Element &element, const WritingState &state) { + const Frame frame = element.as_frame(); + const GraphicStyle style = frame.style(); + + switch (frame.shape_type()) { + case ShapeType::none: + translate_plain_frame(frame, style, state); + break; + case ShapeType::rect: + translate_rect(frame, style, state); + break; + case ShapeType::ellipse: + translate_ellipse(frame, style, state); + break; + case ShapeType::line: + translate_line(frame, style, state); + break; + case ShapeType::custom: + translate_custom_shape(frame, style, state); + break; + } +} + } // namespace odr::internal diff --git a/src/odr/internal/html/document_element.hpp b/src/odr/internal/html/document_element.hpp index ec0cd1966..4080cf347 100644 --- a/src/odr/internal/html/document_element.hpp +++ b/src/odr/internal/html/document_element.hpp @@ -50,10 +50,7 @@ void translate_list(const Element &element, const WritingState &state); void translate_list_item(const Element &element, const WritingState &state); void translate_table(const Element &element, const WritingState &state); void translate_image(const Element &element, const WritingState &state); +/// Writes the box, and the svg outline of whatever shape it draws. void translate_frame(const Element &element, const WritingState &state); -void translate_rect(const Element &element, const WritingState &state); -void translate_line(const Element &element, const WritingState &state); -void translate_circle(const Element &element, const WritingState &state); -void translate_custom_shape(const Element &element, const WritingState &state); } // namespace odr::internal::html diff --git a/src/odr/internal/html/document_style.cpp b/src/odr/internal/html/document_style.cpp index 5293557c1..42ed33177 100644 --- a/src/odr/internal/html/document_style.cpp +++ b/src/odr/internal/html/document_style.cpp @@ -606,45 +606,31 @@ std::string html::translate_frame_properties(const Frame &frame) { return result; } -std::string html::translate_rect_properties(const Rect &rect) { +std::string html::translate_shape_properties(const Frame &frame) { std::string result; result += "position:absolute;"; - result += "left:" + rect.x().to_string() + ";"; - result += "top:" + rect.y().to_string() + ";"; - result += "width:" + rect.width().to_string() + ";"; - result += "height:" + rect.height().to_string() + ";"; - result += translate_drawing_transform(rect.transform()); - return result; -} - -std::string html::translate_circle_properties(const Circle &circle) { - std::string result; - result += "position:absolute;"; - result += "left:" + circle.x().to_string() + ";"; - result += "top:" + circle.y().to_string() + ";"; - result += "width:" + circle.width().to_string() + ";"; - result += "height:" + circle.height().to_string() + ";"; - result += translate_drawing_transform(circle.transform()); - return result; -} - -std::string -html::translate_custom_shape_properties(const CustomShape &custom_shape) { - std::string result; - result += "position:absolute;"; - if (const std::optional x = custom_shape.x(); x.has_value()) { + if (const std::optional x = frame.x(); x.has_value()) { result += "left:" + x->to_string() + ";"; } else { result += "left:0;"; } - if (const std::optional y = custom_shape.y(); y.has_value()) { + if (const std::optional y = frame.y(); y.has_value()) { result += "top:" + y->to_string() + ";"; } else { result += "top:0;"; } - result += "width:" + custom_shape.width().to_string() + ";"; - result += "height:" + custom_shape.height().to_string() + ";"; - result += translate_drawing_transform(custom_shape.transform()); + if (const std::optional width = frame.width(); width.has_value()) { + result += "width:" + width->to_string() + ";"; + } else { + result += "width:0;"; + } + if (const std::optional height = frame.height(); + height.has_value()) { + result += "height:" + height->to_string() + ";"; + } else { + result += "height:0;"; + } + result += translate_drawing_transform(frame.transform()); return result; } diff --git a/src/odr/internal/html/document_style.hpp b/src/odr/internal/html/document_style.hpp index 51c7a1b18..e0feaac6b 100644 --- a/src/odr/internal/html/document_style.hpp +++ b/src/odr/internal/html/document_style.hpp @@ -14,9 +14,6 @@ enum class FontPosition; enum class BreakType; class Frame; -class Rect; -class Circle; -class CustomShape; struct TextStyle; struct ParagraphStyle; @@ -61,8 +58,8 @@ std::string translate_drawing_transform(const std::optional &transform); std::string translate_frame_properties(const Frame &frame); -std::string translate_rect_properties(const Rect &rect); -std::string translate_circle_properties(const Circle &circle); -std::string translate_custom_shape_properties(const CustomShape &custom_shape); +/// The absolute box a frame drawing a shape takes, rather than the placement +/// @ref translate_frame_properties gives a plain one. +std::string translate_shape_properties(const Frame &frame); } // namespace odr::internal::html diff --git a/src/odr/internal/iwork/PLAN.md b/src/odr/internal/iwork/PLAN.md index 5e5981fc2..f297ba3ee 100644 --- a/src/odr/internal/iwork/PLAN.md +++ b/src/odr/internal/iwork/PLAN.md @@ -268,7 +268,7 @@ against UTF-8 text, which the parser translates in one pass. ## Stage 4 — drawables, images, frames - drawable archives carry a geometry (position, size, transform) and a content - reference → `Frame` plus `Image`, `Rect`, `Line`, `CustomShape` as they map. + reference → a `Frame` naming its `ShapeType`, plus `Image` as it maps. - images resolve to a `Data/` entry by name; hand the zip entry through unchanged. - Pages **page-layout mode** falls out here: it is drawables on pages with no diff --git a/src/odr/internal/odf/AGENTS.md b/src/odr/internal/odf/AGENTS.md index de0bb5678..691a69845 100644 --- a/src/odr/internal/odf/AGENTS.md +++ b/src/odr/internal/odf/AGENTS.md @@ -43,6 +43,12 @@ generic type — `text:h` → paragraph, `text:section`/`toc`/date fields → `g `draw:g` → frame. Container types get bespoke children-parsers (`parse_presentation_children` walks only `draw:page`, etc.). +Every `draw:*` shape is a `frame` too, its kind recorded beside the element by +`create_shape_element` and read back by `frame_shape_type`: `draw:rect`/ +`draw:caption` → `rect`, `draw:line`/`draw:measure` → `line`, `draw:circle`/ +`draw:ellipse` → `ellipse` unless `draw:kind` cuts it, and everything whose +geometry is drawn rather than named → `custom`. + **Text runs are coalesced.** A maximal run of consecutive text nodes (`node_pcdata`, `text:s`, `text:tab`) becomes **one** `text` Element spanning `[first, last]` (the end stored in `Text.last`); `text:line-break` is its own diff --git a/src/odr/internal/odf/odf_document.cpp b/src/odr/internal/odf/odf_document.cpp index e02367fbb..87dddbc1e 100644 --- a/src/odr/internal/odf/odf_document.cpp +++ b/src/odr/internal/odf/odf_document.cpp @@ -178,9 +178,7 @@ using AdapterBase = internal::RegistryElementAdapter< abstract::LinkAdapter, abstract::BookmarkAdapter, abstract::ListAdapter, abstract::ListItemAdapter, abstract::TableAdapter, abstract::TableColumnAdapter, abstract::TableRowAdapter, - abstract::TableCellAdapter, abstract::FrameAdapter, abstract::RectAdapter, - abstract::LineAdapter, abstract::CircleAdapter, - abstract::CustomShapeAdapter, abstract::ImageAdapter>; + abstract::TableCellAdapter, abstract::FrameAdapter, abstract::ImageAdapter>; class ElementAdapter final : public AdapterBase { public: @@ -658,6 +656,10 @@ class ElementAdapter final : public AdapterBase { return get_partial_style(element_id).table_cell_style; } + [[nodiscard]] ShapeType + frame_shape_type(const ElementIdentifier element_id) const override { + return m_registry->shape_type(element_id); + } [[nodiscard]] AnchorType frame_anchor_type(const ElementIdentifier element_id) const override { const pugi::xml_node node = get_node(element_id); @@ -679,115 +681,6 @@ class ElementAdapter final : public AdapterBase { } [[nodiscard]] std::optional frame_x(const ElementIdentifier element_id) const override { - return read_measure(get_node(element_id).attribute("svg:x")); - } - [[nodiscard]] std::optional - frame_y(const ElementIdentifier element_id) const override { - return read_measure(get_node(element_id).attribute("svg:y")); - } - [[nodiscard]] std::optional - frame_width(const ElementIdentifier element_id) const override { - return read_measure(get_node(element_id).attribute("svg:width")); - } - [[nodiscard]] std::optional - frame_height(const ElementIdentifier element_id) const override { - return read_measure(get_node(element_id).attribute("svg:height")); - } - [[nodiscard]] std::optional - frame_z_index(const ElementIdentifier element_id) const override { - const pugi::xml_attribute attribute = - get_node(element_id).attribute("draw:z-index"); - if (!attribute) { - return std::nullopt; - } - return static_cast(attribute.as_int()); - } - [[nodiscard]] std::optional - frame_transform(const ElementIdentifier element_id) const override { - return read_transform(get_node(element_id)); - } - [[nodiscard]] GraphicStyle - frame_style(const ElementIdentifier element_id) const override { - return get_intermediate_style(element_id).graphic_style; - } - - [[nodiscard]] Measure - rect_x(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:x")); - } - [[nodiscard]] Measure - rect_y(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:y")); - } - [[nodiscard]] Measure - rect_width(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:width")); - } - [[nodiscard]] Measure - rect_height(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:height")); - } - [[nodiscard]] std::optional - rect_transform(const ElementIdentifier element_id) const override { - return read_transform(get_node(element_id)); - } - [[nodiscard]] GraphicStyle - rect_style(const ElementIdentifier element_id) const override { - return get_intermediate_style(element_id).graphic_style; - } - - [[nodiscard]] Measure - line_x1(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:x1")); - } - [[nodiscard]] Measure - line_y1(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:y1")); - } - [[nodiscard]] Measure - line_x2(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:x2")); - } - [[nodiscard]] Measure - line_y2(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:y2")); - } - [[nodiscard]] std::optional - line_transform(const ElementIdentifier element_id) const override { - return read_transform(get_node(element_id)); - } - [[nodiscard]] GraphicStyle - line_style(const ElementIdentifier element_id) const override { - return get_intermediate_style(element_id).graphic_style; - } - - [[nodiscard]] Measure - circle_x(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:x")); - } - [[nodiscard]] Measure - circle_y(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:y")); - } - [[nodiscard]] Measure - circle_width(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:width")); - } - [[nodiscard]] Measure - circle_height(const ElementIdentifier element_id) const override { - return read_measure_or_zero(get_node(element_id).attribute("svg:height")); - } - [[nodiscard]] std::optional - circle_transform(const ElementIdentifier element_id) const override { - return read_transform(get_node(element_id)); - } - [[nodiscard]] GraphicStyle - circle_style(const ElementIdentifier element_id) const override { - return get_intermediate_style(element_id).graphic_style; - } - - [[nodiscard]] std::optional - custom_shape_x(const ElementIdentifier element_id) const override { const pugi::xml_node node = get_node(element_id); if (const std::optional measure = read_measure(node.attribute("svg:x"))) { @@ -799,7 +692,7 @@ class ElementAdapter final : public AdapterBase { return {}; } [[nodiscard]] std::optional - custom_shape_y(const ElementIdentifier element_id) const override { + frame_y(const ElementIdentifier element_id) const override { const pugi::xml_node node = get_node(element_id); if (const std::optional measure = read_measure(node.attribute("svg:y"))) { @@ -810,38 +703,65 @@ class ElementAdapter final : public AdapterBase { } return {}; } - [[nodiscard]] Measure - custom_shape_width(const ElementIdentifier element_id) const override { + [[nodiscard]] std::optional + frame_width(const ElementIdentifier element_id) const override { const pugi::xml_node node = get_node(element_id); - if (const pugi::xml_attribute attribute = node.attribute("svg:width")) { - return read_measure_or_zero(attribute); + if (const std::optional measure = + read_measure(node.attribute("svg:width"))) { + return measure; } if (const std::optional box = connector_box(node)) { return hundredth_millimetres(box->width); } - return Measure(0, DynamicUnit()); + return {}; } - [[nodiscard]] Measure - custom_shape_height(const ElementIdentifier element_id) const override { + [[nodiscard]] std::optional + frame_height(const ElementIdentifier element_id) const override { const pugi::xml_node node = get_node(element_id); - if (const pugi::xml_attribute attribute = node.attribute("svg:height")) { - return read_measure_or_zero(attribute); + if (const std::optional measure = + read_measure(node.attribute("svg:height"))) { + return measure; } if (const std::optional box = connector_box(node)) { return hundredth_millimetres(box->height); } - return Measure(0, DynamicUnit()); + return {}; } - [[nodiscard]] std::optional - custom_shape_path(const ElementIdentifier element_id) const override { - return read_path(get_node(element_id)); + [[nodiscard]] std::optional + frame_z_index(const ElementIdentifier element_id) const override { + const pugi::xml_attribute attribute = + get_node(element_id).attribute("draw:z-index"); + if (!attribute) { + return std::nullopt; + } + return static_cast(attribute.as_int()); } [[nodiscard]] std::optional - custom_shape_transform(const ElementIdentifier element_id) const override { + frame_transform(const ElementIdentifier element_id) const override { return read_transform(get_node(element_id)); } + [[nodiscard]] std::optional + frame_path(const ElementIdentifier element_id) const override { + if (m_registry->shape_type(element_id) != ShapeType::custom) { + return {}; + } + return read_path(get_node(element_id)); + } + [[nodiscard]] std::optional + frame_line(const ElementIdentifier element_id) const override { + if (m_registry->shape_type(element_id) != ShapeType::line) { + return {}; + } + const pugi::xml_node node = get_node(element_id); + return DrawingLine{ + .x1 = read_measure_or_zero(node.attribute("svg:x1")), + .y1 = read_measure_or_zero(node.attribute("svg:y1")), + .x2 = read_measure_or_zero(node.attribute("svg:x2")), + .y2 = read_measure_or_zero(node.attribute("svg:y2")), + }; + } [[nodiscard]] GraphicStyle - custom_shape_style(const ElementIdentifier element_id) const override { + frame_style(const ElementIdentifier element_id) const override { return get_intermediate_style(element_id).graphic_style; } diff --git a/src/odr/internal/odf/odf_element_registry.cpp b/src/odr/internal/odf/odf_element_registry.cpp index 772254686..caf7afebd 100644 --- a/src/odr/internal/odf/odf_element_registry.cpp +++ b/src/odr/internal/odf/odf_element_registry.cpp @@ -13,6 +13,14 @@ ElementRegistry::create_element(const ElementType type, return {element_id, element}; } +std::tuple +ElementRegistry::create_shape_element(const ShapeType shape_type, + const pugi::xml_node node) { + const auto &[element_id, element] = create_element(ElementType::frame, node); + m_shape_types.emplace(element_id, shape_type); + return {element_id, element}; +} + std::tuple ElementRegistry::create_text_element(const pugi::xml_node first_node, @@ -192,6 +200,12 @@ ElementRegistry::Sheet::cell_node(const std::uint32_t column, return {}; } +[[nodiscard]] ShapeType +ElementRegistry::shape_type(const ElementIdentifier id) const { + const ShapeType *entry = m_shape_types.find(id); + return entry != nullptr ? *entry : ShapeType::none; +} + void ElementRegistry::set_list_type(const ElementIdentifier id, const ListType type) { check_element_id(id); diff --git a/src/odr/internal/odf/odf_element_registry.hpp b/src/odr/internal/odf/odf_element_registry.hpp index bc77d2258..db1794229 100644 --- a/src/odr/internal/odf/odf_element_registry.hpp +++ b/src/odr/internal/odf/odf_element_registry.hpp @@ -99,6 +99,9 @@ class ElementRegistry final std::tuple create_element(ElementType type, pugi::xml_node node); + /// A `frame` drawing @p shape_type rather than being a plain box. + std::tuple + create_shape_element(ShapeType shape_type, pugi::xml_node node); std::tuple create_text_element(pugi::xml_node first_node, pugi::xml_node last_node); std::tuple @@ -140,6 +143,8 @@ class ElementRegistry final return m_sheet_cells.find(id); } + [[nodiscard]] ShapeType shape_type(ElementIdentifier id) const; + void set_list_type(ElementIdentifier id, ListType type); void set_list_marker(ElementIdentifier id, ListMarker marker); @@ -155,6 +160,7 @@ class ElementRegistry final SortedSideTable m_tables; SortedSideTable m_sheets; SortedSideTable m_sheet_cells; + SortedSideTable m_shape_types; // out of id order: written when a list is resolved, not when it is parsed SideTable m_list_types; SideTable m_list_markers; diff --git a/src/odr/internal/odf/odf_parser.cpp b/src/odr/internal/odf/odf_parser.cpp index dcb0baff8..48c301734 100644 --- a/src/odr/internal/odf/odf_parser.cpp +++ b/src/odr/internal/odf/odf_parser.cpp @@ -52,6 +52,21 @@ parse_element_tree(ElementRegistry ®istry, const ElementType type, return {element_id, node.next_sibling()}; } +std::tuple +parse_shape_tree(ElementRegistry ®istry, const ShapeType shape_type, + const pugi::xml_node node, + const ChildrenParser &children_parser) { + if (!node) { + return {null_element_id, pugi::xml_node()}; + } + + const auto &[element_id, _] = registry.create_shape_element(shape_type, node); + + children_parser(registry, element_id, node); + + return {element_id, node.next_sibling()}; +} + bool is_text_node(const pugi::xml_node node) { if (!node) { return false; @@ -253,10 +268,12 @@ parse_replaceable_image(ElementRegistry ®istry, const pugi::xml_node node) { std::tuple parse_elliptical_element(ElementRegistry ®istry, const pugi::xml_node node) { const std::string_view kind = node.attribute("draw:kind").value(); - const ElementType type = (kind == "arc" || kind == "cut" || kind == "section") - ? ElementType::custom_shape - : ElementType::circle; - return parse_element_tree(registry, type, node, parse_any_element_children); + const ShapeType shape_type = + (kind == "arc" || kind == "cut" || kind == "section") + ? ShapeType::custom + : ShapeType::ellipse; + return parse_shape_tree(registry, shape_type, node, + parse_any_element_children); } void parse_presentation_children(ElementRegistry ®istry, @@ -298,6 +315,11 @@ parse_any_element_tree(ElementRegistry ®istry, const pugi::xml_node node) { return parse_element_tree(r, type, n, children_parser); }; }; + const auto create_shape_tree_parser = [](const ShapeType shape_type) { + return [shape_type](ElementRegistry &r, const pugi::xml_node n) { + return parse_shape_tree(r, shape_type, n, parse_any_element_children); + }; + }; static std::unordered_map parser_table{ {"office:text", create_default_tree_parser(ElementType::root)}, @@ -348,20 +370,18 @@ parse_any_element_tree(ElementRegistry ®istry, const pugi::xml_node node) { {"draw:image", parse_replaceable_image}, // An embedded object renders as the image its own part is drawn to. {"draw:object", create_default_tree_parser(ElementType::image)}, - {"draw:rect", create_default_tree_parser(ElementType::rect)}, - {"draw:line", create_default_tree_parser(ElementType::line)}, + {"draw:rect", create_shape_tree_parser(ShapeType::rect)}, + {"draw:line", create_shape_tree_parser(ShapeType::line)}, {"draw:circle", parse_elliptical_element}, - {"draw:custom-shape", - create_default_tree_parser(ElementType::custom_shape)}, + {"draw:custom-shape", create_shape_tree_parser(ShapeType::custom)}, // A shape whose geometry is given rather than named is a custom shape. - {"draw:path", create_default_tree_parser(ElementType::custom_shape)}, - {"draw:polygon", create_default_tree_parser(ElementType::custom_shape)}, - {"draw:polyline", create_default_tree_parser(ElementType::custom_shape)}, - {"draw:regular-polygon", - create_default_tree_parser(ElementType::custom_shape)}, - {"draw:connector", create_default_tree_parser(ElementType::custom_shape)}, - {"draw:caption", create_default_tree_parser(ElementType::rect)}, - {"draw:measure", create_default_tree_parser(ElementType::line)}, + {"draw:path", create_shape_tree_parser(ShapeType::custom)}, + {"draw:polygon", create_shape_tree_parser(ShapeType::custom)}, + {"draw:polyline", create_shape_tree_parser(ShapeType::custom)}, + {"draw:regular-polygon", create_shape_tree_parser(ShapeType::custom)}, + {"draw:connector", create_shape_tree_parser(ShapeType::custom)}, + {"draw:caption", create_shape_tree_parser(ShapeType::rect)}, + {"draw:measure", create_shape_tree_parser(ShapeType::line)}, {"draw:ellipse", parse_elliptical_element}, {"draw:text-box", create_default_tree_parser(ElementType::group)}, {"draw:g", create_default_tree_parser(ElementType::frame)}, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3ebb3af8f..45577d5f3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,6 +76,7 @@ add_executable(odr_test "src/internal/odf/odf_chart_test.cpp" "src/internal/odf/odf_enhanced_geometry_test.cpp" "src/internal/odf/odf_flat_file_test.cpp" + "src/internal/odf/odf_frame_test.cpp" "src/internal/odf/odf_geometry_test.cpp" "src/internal/odf/odf_sheet_repeat_test.cpp" "src/internal/odf/odf_table_test.cpp" diff --git a/test/src/internal/odf/odf_frame_test.cpp b/test/src/internal/odf/odf_frame_test.cpp new file mode 100644 index 000000000..df23eae38 --- /dev/null +++ b/test/src/internal/odf/odf_frame_test.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace odr; +using namespace odr::internal; + +namespace { + +std::string write_odg(const std::string &name, const std::string &body) { + const std::string content = + R"()" + R"()" + R"()" + + body + + R"()" + R"()"; + + const std::string path = (std::filesystem::current_path() / name).string(); + + zip::ZipArchive zip; + zip.insert_file(std::end(zip), RelPath("mimetype"), + std::make_shared( + "application/vnd.oasis.opendocument.graphics")); + zip.insert_file(std::end(zip), RelPath("content.xml"), + std::make_shared(content)); + std::ofstream out(path); + zip.save(out); + + return path; +} + +std::vector shapes_of(const Document &document) { + std::vector result; + for (const Element child : document.root_element().first_child().children()) { + result.push_back(child.as_frame()); + } + return result; +} + +} // namespace + +TEST(OdfFrame, every_draw_shape_is_a_frame_naming_its_kind) { + const std::string path = write_odg( + "odf_shape_kinds.odg", + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()" + R"()"); + + const Document document = odr::open(path).as_document_file().document(); + const std::vector shapes = shapes_of(document); + + std::vector kinds; + for (const Frame &shape : shapes) { + EXPECT_EQ(shape.type(), ElementType::frame); + kinds.push_back(shape.shape_type()); + } + + EXPECT_EQ(kinds, + (std::vector{ + ShapeType::none, ShapeType::none, ShapeType::rect, + ShapeType::rect, ShapeType::ellipse, ShapeType::ellipse, + ShapeType::custom, ShapeType::line, ShapeType::line, + ShapeType::custom, ShapeType::custom, ShapeType::custom})); +} + +TEST(OdfFrame, a_rect_carries_its_box_and_the_anchor_a_frame_has) { + const std::string path = write_odg( + "odf_shape_rect.odg", + R"()"); + + const Document document = odr::open(path).as_document_file().document(); + const Frame rect = shapes_of(document).at(0); + + EXPECT_EQ(rect.shape_type(), ShapeType::rect); + EXPECT_EQ(rect.anchor_type(), AnchorType::at_paragraph); + EXPECT_EQ(rect.z_index(), 3); + ASSERT_TRUE(rect.x().has_value()); + EXPECT_EQ(rect.x()->to_string(), "1cm"); + EXPECT_EQ(rect.width()->to_string(), "3cm"); + EXPECT_FALSE(rect.line().has_value()); + EXPECT_FALSE(rect.path().has_value()); +} + +TEST(OdfFrame, a_line_states_its_two_ends) { + const std::string path = write_odg( + "odf_shape_line.odg", + R"()"); + + const Document document = odr::open(path).as_document_file().document(); + const Frame line = shapes_of(document).at(0); + + ASSERT_TRUE(line.line().has_value()); + EXPECT_EQ(line.line()->x1.to_string(), "1cm"); + EXPECT_EQ(line.line()->y1.to_string(), "2cm"); + EXPECT_EQ(line.line()->x2.to_string(), "3cm"); + EXPECT_EQ(line.line()->y2.to_string(), "4cm"); +} + +TEST(OdfFrame, a_custom_shape_carries_its_outline) { + const std::string path = + write_odg("odf_shape_custom.odg", + R"()" + R"()" + R"()"); + + const Document document = odr::open(path).as_document_file().document(); + const Frame shape = shapes_of(document).at(0); + + EXPECT_EQ(shape.shape_type(), ShapeType::custom); + ASSERT_TRUE(shape.path().has_value()); + EXPECT_EQ(shape.path()->width, 100); + EXPECT_FALSE(shape.path()->data.empty()); +}