You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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:
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.
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.
The public element model is compact almost everywhere. The one place it is not is the drawing
elements:
ElementTypecarriesframe,image,rect,line,circleandcustom_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_shapeEvery other engine already funnels every shape into
frame:ooxml/presentationp:sp,p:graphicFrame→frame(ooxml_presentation_parser.cpp:167)ooxml/textw:drawing→frame(ooxml_text_parser.cpp:222)ooxml/spreadsheetframe(ooxml_spreadsheet_parser.cpp:266)oldms/presentationframe(ppt_element_registry.cpp:39)iworkframe(iwork_element_registry.cpp:47)odfframe+ 4 shape types (odf_parser.cpp:302-310)frameis already the generic-shape element in five of six engines. The split is an ODF-parserartifact that leaked into the public API — and it means
p:spwitha:prstGeom, and the.pptODRAW 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_*andcustom_shape_*are the same fourread_measurecalls onsvg:x/svg:y/svg:width/svg:height. Indocument_style.cpp:515-533,translate_rect_propertiesandtranslate_circle_propertiesarecharacter-for-character identical:
The only real differences across the three are
Measurevsstd::optional<Measure>and whichone-line SVG literal the translator emits.
3. The split causes a live rendering bug
Framehasanchor_type()andz_index().Rect,CircleandCustomShapehave neither, sotranslate_rect_propertiesand friends hardcodeposition:absolute.test/data/input/odr-public/odt/XYZ-Rechnung.odt:and the committed reference output
(
odr-public/output/odt/XYZ-Rechnung.odt/document.html):No
z-index, and offsets that ODF defines relative to the anchoring paragraph are appliedrelative to the page, so the shape lands at the top of the page. A
draw:framewith the sameattributes renders correctly, because
Framemodels the anchor. Shapes do carry anchors in realtext documents —
odr-private/odt/draw-undefined-1.odtand the file above both usetext: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):None of
draw:ellipse,draw:path,draw:connector,draw:polygon,draw:polyline,draw:regular-polygon,draw:caption,draw:measureare in the parser table — andparse_any_element_children(odf_parser.cpp:31) skips an unmapped node together with its wholesubtree, so a shape with text in it loses the text too.
draw:ellipsein particular is whatLibreOffice writes for most round shapes; only
draw:circleis handled.Adding any one of them today costs: an
ElementTypevalue, a public class, anas_*(), anabstract adapter interface, an
*_adapter()dispatcher, the engine impl, an HTML translator, andmirrors 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
frameavoids a rename ripple through four bindinglayers, and five engines already use it this way:
translate_framegrows oneswitch (frame.shape_type())choosing the SVG overlay — and the codeeach branch needs is already written, sitting in
translate_rect/translate_circle/translate_line(html/document_element.cpp:598-664).What it buys:
−3 duplicate style writers, plus the Python / JNI / ObjC mirrors of all of them.
Frame's placement logic.draw:ellipse,draw:path,draw:polygon,draw:connectorbecome 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.
The one thing given up is
Rect/Circle's non-optionalMeasuregeometry becomingstd::optional<Measure>.CustomShapealready returnsstd::optionalfor x/y andtranslate_custom_shape_propertiesalready defaults to0, so this is a non-issue in practice.Alternative, if overloading the name
frameis unappealing:frame+ a newshapeelementcarrying
ShapeType. Two elements instead of five. Defensible, but it re-splits a distinction noother 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_breakis parsed and never rendered. ODF (text:soft-page-break), RTF (\page) and.doc(\x0C) all emitElementType::page_break, buthtml::translate_elementhas no casefor it and it falls into
default: // TODO log(html/document_element.cpp:74). Three enginesproduce it; the renderer drops it silently.
groupmeans two different things.ElementType::groupis the transparent-passthroughcontainer (
text:section,text:table-of-content,w:sdt,p:txBody) — the renderer justrecurses into children. But ODF's actual drawing group
draw:gis mapped toframe(
odf_parser.cpp:310), where it producesdisplay:block;position:absolute;with noleft/top/width/height, because a group carries none of those attributes. 85 occurrences in the
corpus. Either
draw:gshould begroup, or the merged shape element needs agroupkind.ElementType::rootand theTextRoothandle. Presentation, spreadsheet and drawing rootsare all
ElementType::root, soas_text_root()succeeds on an.odproot and returns aTextRootwithpage_layout()/first_master_page(). Harmless in practice, but the nameclaims more than it delivers.
Annotations/comments and text fields (
text:page-numberis commented out atodf_parser.cpp:293)are the notable missing elements, but those are already tracked per module and are coverage
gaps rather than model problems.
Related