Skip to content

Element model: the five drawing element types are one concept — collapse rect/line/circle/custom_shape into frame #773

Description

@andiwand

The public element model is compact almost everywhere. The one place it is not is the drawing
elements: ElementType carries frame, image, rect, line, circle and custom_shape,
and four of those six are a distinction only the ODF parser makes. They should be one element
with a shape-kind discriminator.

This is the API-shape counterpart to #771, which covers which ODF draw elements get parsed and
how their geometry is rendered. Collapsing the model first makes #771's "missing shape elements"
step a one-line parser-table entry each instead of a full vertical slice per shape.

1. Only ODF ever produces rect / line / circle / custom_shape

Every other engine already funnels every shape into frame:

engine shape mapping
ooxml/presentation p:sp, p:graphicFrameframe (ooxml_presentation_parser.cpp:167)
ooxml/text w:drawingframe (ooxml_text_parser.cpp:222)
ooxml/spreadsheet drawing anchor → frame (ooxml_spreadsheet_parser.cpp:266)
oldms/presentation frame (ppt_element_registry.cpp:39)
iwork frame (iwork_element_registry.cpp:47)
odf frame + 4 shape types (odf_parser.cpp:302-310)

frame is already the generic-shape element in five of six engines. The split is an ODF-parser
artifact that leaked into the public API — and it means p:sp with a:prstGeom, and the
.ppt ODRAW shape types, have nowhere to say what they are.

2. The three box shapes are literally the same code

In odf_document.cpp:841-917, rect_*, circle_* and custom_shape_* are the same four
read_measure calls on svg:x / svg:y / svg:width / svg:height. In
document_style.cpp:515-533, translate_rect_properties and translate_circle_properties are
character-for-character identical:

std::string html::translate_rect_properties(const Rect &rect) {
  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() + ";";
  return result;
}

The only real differences across the three are Measure vs std::optional<Measure> and which
one-line SVG literal the translator emits.

3. The split causes a live rendering bug

Frame has anchor_type() and z_index(). Rect, Circle and CustomShape have neither, so
translate_rect_properties and friends hardcode position:absolute.

test/data/input/odr-public/odt/XYZ-Rechnung.odt:

<draw:custom-shape text:anchor-type="paragraph" draw:z-index="1"
                   svg:width="17.146cm" svg:height="1.668cm"
                   svg:x="-0.06cm" svg:y="0.247cm">

and the committed reference output
(odr-public/output/odt/XYZ-Rechnung.odt/document.html):

position:absolute;left:-0.06cm;top:0.247cm;width:17.146cm;height:1.668cm;…

No z-index, and offsets that ODF defines relative to the anchoring paragraph are applied
relative to the page, so the shape lands at the top of the page. A draw:frame with the same
attributes renders correctly, because Frame models the anchor. Shapes do carry anchors in real
text documents — odr-private/odt/draw-undefined-1.odt and the file above both use
text:anchor-type="paragraph".

4. The taxonomy does not cover its own format

Counting ODF draw elements across the whole test corpus (test/data/input, odt/ods/odp/odg):

draw:custom-shape 324   draw:g          85   draw:path        3
draw:line         168   draw:circle     13   draw:connector   2
draw:rect          92                        draw:ellipse     1
draw:polygon / polyline / regular-polygon / caption / measure: 0 in corpus, common in the wild

None of draw:ellipse, draw:path, draw:connector, draw:polygon, draw:polyline,
draw:regular-polygon, draw:caption, draw:measure are in the parser table — and
parse_any_element_children (odf_parser.cpp:31) skips an unmapped node together with its whole
subtree
, so a shape with text in it loses the text too. draw:ellipse in particular is what
LibreOffice writes for most round shapes; only draw:circle is handled.

Adding any one of them today costs: an ElementType value, a public class, an as_*(), an
abstract adapter interface, an *_adapter() dispatcher, the engine impl, an HTML translator, and
mirrors in Python (bind_document.cpp), JNI (a Java class + glue) and ObjC
(ODRDocumentElement.h/.mm). That is the price of a discriminator a plain enum field would carry.

Proposal

Collapse them into one. Keeping the name frame avoids a rename ripple through four binding
layers, and five engines already use it this way:

enum class ShapeType {
  none,        ///< a plain container frame
  rect, ellipse, custom, line, polygon, polyline, path, connector,
};

class Frame … {
  [[nodiscard]] ShapeType shape_type() const;                    // new
  [[nodiscard]] std::optional<std::string> points() const;       // new: line/polyline/polygon/path

  [[nodiscard]] AnchorType anchor_type() const;                  // already there
  [[nodiscard]] std::optional<Measure> x/y/width/height() const; // already there
  [[nodiscard]] std::optional<std::int32_t> z_index() const;     // already there
  [[nodiscard]] GraphicStyle style() const;                      // already there
};

translate_frame grows one switch (frame.shape_type()) choosing the SVG overlay — and the code
each branch needs is already written, sitting in translate_rect / translate_circle /
translate_line (html/document_element.cpp:598-664).

What it buys:

  • −4 public classes, −4 abstract adapter interfaces, −18 near-duplicate ODF methods,
    −3 duplicate style writers
    , plus the Python / JNI / ObjC mirrors of all of them.
  • The anchoring and z-index bug in §3 disappears: shapes inherit Frame's placement logic.
  • draw:ellipse, draw:path, draw:polygon, draw:connector become one parser-table line each
    (ODF drawings beyond SVM: enhanced geometry, the missing shape elements, embedded chart objects, and transforms #771 step 2) instead of a full vertical slice.
  • Non-ODF engines get somewhere to put the shape kind they already know.

The one thing given up is Rect/Circle's non-optional Measure geometry becoming
std::optional<Measure>. CustomShape already returns std::optional for x/y and
translate_custom_shape_properties already defaults to 0, so this is a non-issue in practice.

Alternative, if overloading the name frame is unappealing: frame + a new shape element
carrying ShapeType. Two elements instead of five. Defensible, but it re-splits a distinction no
other engine makes.

This is a breaking public-API change (four classes and four enum values removed) across C++,
Python, JNI/Java and ObjC, so it wants to land with a release that says so.

Other findings from the same review

Small and separable — happy to split these out if preferred.

  • page_break is parsed and never rendered. ODF (text:soft-page-break), RTF (\page) and
    .doc (\x0C) all emit ElementType::page_break, but html::translate_element has no case
    for it and it falls into default: // TODO log (html/document_element.cpp:74). Three engines
    produce it; the renderer drops it silently.
  • group means two different things. ElementType::group is the transparent-passthrough
    container (text:section, text:table-of-content, w:sdt, p:txBody) — the renderer just
    recurses into children. But ODF's actual drawing group draw:g is mapped to frame
    (odf_parser.cpp:310), where it produces display:block;position:absolute; with no
    left/top/width/height, because a group carries none of those attributes. 85 occurrences in the
    corpus. Either draw:g should be group, or the merged shape element needs a group kind.
  • ElementType::root and the TextRoot handle. Presentation, spreadsheet and drawing roots
    are all ElementType::root, so as_text_root() succeeds on an .odp root and returns a
    TextRoot with page_layout() / first_master_page(). Harmless in practice, but the name
    claims more than it delivers.

Annotations/comments and text fields (text:page-number is commented out at odf_parser.cpp:293)
are the notable missing elements, but those are already tracked per module and are coverage
gaps rather than model problems.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions