Skip to content

feat(odf): decode flat xml OpenDocument files - #731

Open
andiwand wants to merge 2 commits into
mainfrom
feat/odf-flat-xml
Open

feat(odf): decode flat xml OpenDocument files#731
andiwand wants to merge 2 commits into
mainfrom
feat/odf-flat-xml

Conversation

@andiwand

Copy link
Copy Markdown
Member

.fodt, .fodp, .fods and .fodg were only extension and mimetype aliases on the zip-backed rows. The table therefore promised open, translate_html, edit and save for a file that in fact fell through to the xml source view — and a host that trusted the extension and named the type got NoOpenDocumentFile instead. This makes the promise true.

How

A flat document's one office:document root carries what content.xml and styles.xml carry between them, so odf::Document gains a constructor that hands that root in as both roots. Everything downstream — parse_tree, StyleRegistry, list numbering, the adapter — is the existing code, shared with the packaged path via a new init_.

Recognition rides on the parse xml::XmlFile already did, right next to svg's: the root element is the only thing that tells either from any other xml. is_flat_opendocument_file reads it off that tree; FlatOpenDocumentFile then reparses with the document parse options, which a source view's (parse_full | parse_ws_pcdata_single) cannot substitute for — whitespace-only pcdata is text in one and nothing in the other.

Without a package there is no filesystem, so two things differ:

  • Images come base64 encoded in office:binary-data. The adapter decodes them lazily, and does so in a package too, where they are equally legal but were previously reported as external.
  • save re-serialises the one tree instead of rebuilding a zip.

Flat files are never encrypted, so decrypt throws NotEncryptedError.

Verification

Ten unit tests on inline flat-xml literals cover the four types, the recogniser (including what must not open as flat), tree and style resolution from the single root, embedded and linked images, and the save round-trip.

Beyond that, LibreOffice's own flat export of eleven files across the four types was rendered and compared against the packaged render of the same source:

packaged flat images
about.fodt 635 lines 635 png + jpeg, 85084 B both
image-text-wrap.fodt 596 596 8 png, 4633120 B both
table-span.fodt 634 634 identical output
mixed-layout.fodt 590 590 identical output
style-various-1.fodt 683 683 png, 100792 B both
image-1.fodp 635 635 png, 24876 B both
presentation-yunation.fodp 1198 1198 12 png + jpeg, 7014156 B both
style-various-1.fodp 732 732
file_example_ODS_100.fods 3624 3624
draw.fods 909 909
sample.fodg 1183 1183

Image payloads are byte-identical throughout. The remaining line differences trace to LibreOffice's export, not to the decoder — e.g. it writes svg:stroke-color="#000000" draw:fill-color="#99ccff" into the default graphic style that the packaged file does not have.

Full suite: 995 passed, 0 failed, 8 skipped (the usual svm/wpd ones).

Follow-up, not in here

The corpus in OpenDocument.test has no flat files, so html_output_test gives this no pinned reference coverage. Adding one file per type there plus a pin bump is the natural next step — happy to do it as a stacked change if you want it.

andiwand and others added 2 commits August 23, 2026 18:06
`.fodt`, `.fodp`, `.fods` and `.fodg` were only extension and mimetype
aliases on the zip-backed rows, so the table promised `open`,
`translate_html`, `edit` and `save` for a file that in fact fell through
to the xml source view - or, when a host trusted the extension and named
the type, threw `NoOpenDocumentFile`.

A flat document's one `office:document` root carries what `content.xml`
and `styles.xml` carry between them, so `odf::Document` gains a
constructor that hands that root in as both roots; parse, styles, list
numbering and the adapter are the existing ones. Recognition rides on
the parse `XmlFile` already did, next to svg's - the root element is all
that tells either from any other xml.

Without a package there is no filesystem, so two things differ. Images
come base64 encoded in `office:binary-data`, decoded lazily by the
adapter, which also picks them up in a package where they are equally
legal. And `save` re-serialises the one tree rather than rebuilding a
zip.

Verified against LibreOffice's own flat export of eleven files across
the four types: the rendered html matches the packaged render line for
line, with byte-identical image payloads, and what differs is what
LibreOffice itself writes differently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUmsvGsbfXFgubV2RcpR4P
`office:binary-data` and `xlink:href` can sit on the same `draw:image`.
The short-circuit that reads the markup bytes returned `is_internal()`
before the `make_absolute()` check that rejects an escaping path, while
`image_href` still answered with the raw attribute - so `bring_offline`
wrote the decoded bytes to wherever the href pointed. The bytes in the
markup are the image, so an href beside them names no file of ours and
never reaches the renderer now.

Also from the same review:

- `Document::as_filesystem` answers with an empty filesystem where the
  impl has none instead of throwing `std::invalid_argument`, which every
  flat document did - `odr-cli server foo.fodt` died on it after having
  built its html service. The internal null stays null: `save` and the
  image lookups pick the flat path off it.
- `parse_file_meta` shares `read_entry_count` and
  `document_type_by_file_type` with the flat parse rather than writing
  both out by hand.
- Eight tests: `office:binary-data` in a package, markup bytes beating an
  href in both encodings, the entry counts including a negative,
  `list_file_types`, the empty filesystem, distinct hrefs, and a real
  `html::translate` of a flat document embedded and linked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015E5WrxdYxfoBE7JUotBtqh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant