A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.
PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.
Clicks and the caret inside a turned box still go by its upright text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Three causes of the largest remaining LibreOffice round-trip
differences:
- In-line frames and shapes centred on or hanging from the baseline
(`style:vertical-pos` `middle`, `bottom`, or `from-top` with
`svg:y`, relative to the baseline) were read as standing on it. They
are now read as their run's lowering, which the layout honours, and
written back as `from-top`.
- Leading: LibreOffice also leaves the leading after a paragraph's last
line. It is now added to the paragraph's space after.
- Text box frames didn't derive from LibreOffice's Frame style, without
which LibreOffice ignores a frame's `fo:border` and outlines it in
its default shape colour. They now do, and an empty Frame style is
declared.
LibreOffice round trips (pixel difference): shapes 13,864 → 1,273,
spacing 2,973 → 706, text box 736 → 0. No other test document changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.
A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT form controls (ODF 1.3 §13: controls in `office:forms`, each
placed in the text by a `draw:control`) were dropped on open, values
and all. They now open as WordCraft content controls, each with its
value, entries and selection; its name becomes the tag and its label
the title:
- text fields and text areas: plain text controls;
- check boxes: check boxes;
- radio buttons: check boxes with round symbols;
- list boxes: drop-down lists;
- combo boxes: combo boxes;
- date fields: date controls.
Saved as DOCX they become Word content controls.
Content controls of those kinds in the body are written back as form
controls with a `draw:control` anchor; in headers and footers they stay
their text. LibreOffice shows the round-tripped controls with their
values, and the content validates against the ODF 1.3 schema.
Not read: buttons, images, hidden and other controls. Controls' sizes
and looks aren't kept.
Flow model: new `Inline::Control`, kept only by flows that ask for
boxes (ODT); other formats get its text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A password-protected ODT (ODF 1.3 Part 2 §3.4) failed to open. The
package is now decrypted with the password into a plain one, which the
reader opens as before.
- Supported: AES-256-CBC with a SHA-256 or SHA-1 start key, PBKDF2
with HMAC-SHA-1, and SHA-256 or SHA-1 1K checksums (LibreOffice's
encryption).
- Blowfish (the old default) is refused with a message naming it.
- A missing or wrong password gives the same errors as Word files, so
the app asks for the password.
- Hostile parameters are bounded: iteration count, IV size, ragged
data, inflated size.
Saving with a password (and saving a document opened with one) writes
the ODT encrypted as LibreOffice does. Every file but the `mimetype`
and manifest is deflated and encrypted, each with its own salt and IV,
and the manifest validates against the ODF manifest schema.
LibreOffice opens WordCraft's encrypted files with the password and
refuses a wrong one; WordCraft opens LibreOffice's.
No new dependencies: the AES, SHA, HMAC, miniz and getrandom crates
the workspace already uses for Word encryption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the objects commit found two gaps.
- The header and footer reader wasn't given the package's entry list,
so embedded objects there were still lost on open. It now shares the
body reader's list and manifest, borrowed rather than copied.
- Headers and footers were flattened for writing without drawings, so
shapes, text boxes, charts and kept objects in them were dropped on
save. They now go out as in the body.
The full LibreOffice round-trip harness shows no change on the other
test documents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.
Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
placeholder of the object's size drawn in code (ruled for a
spreadsheet).
- ODT save: written back as the same object, with its manifest entries
and original replacement. LibreOffice renders a round-tripped
spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
linked from the picture's `ole` by a marker. The DOCX writer skips
the marker and writes the picture alone.
Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review of the chart commit: the engine drew opened charts only at the
paragraph level, so a chart that was a member of a `draw:g` group
opened blank. The pass now goes into groups (bounded depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A chart object (`draw:object` whose sub-document is an `office:chart`,
ODF 1.3 §10.4.3, Part 3 §11) used to open as its replacement picture.
It is now read into WordCraft's chart model: column, bar (stacked or
not), line, area, pie, doughnut and scatter charts, with categories,
series names and values (gaps included) taken from the chart's own
table through the series' cell range addresses, plus title, legend
position and data labels. The engine draws opened charts in the
document's theme colours, and Chart Design's commands edit them.
Charts WordCraft can't edit as they are (3D, percent-stacked, mixed
plot types, a second value axis, radar and the like) keep their
picture.
WordCraft charts are written as ODF chart objects shaped like
LibreOffice's: a data table, each series and slice in its accent
colour, pies clockwise from the top. They validate against the ODF 1.3
schema, and LibreOffice draws them like WordCraft does. In flat
documents they are written inline as an `office:document`.
Bug fixed: flat ODT reading ended the body at the first nested
`</office:body>`, so an inline sub-document dropped the text after it.
Element lookup now follows nesting, with a bounded depth.
Flow model: new `Inline::Chart`, kept only by flows that ask for
boxes (ODT); other formats still get the alt text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice's leading (`style:line-spacing`, extra space between lines)
has no WordCraft line spacing mode. It was read as a multiple of a
12 pt line, which spreads the extra space differently from LibreOffice,
so the paragraph started too high. It is now read as at least a 12 pt
line and the leading: the extra space goes above each line, as
LibreOffice draws leading (it also leaves some after the last line,
which isn't followed).
A LibreOffice spacing document went from 6,190 px apart after a round
trip to 2,973, and WordCraft's drawing of it from 8,570 to 5,521.
Regression test: `leading_is_at_least_a_line_and_the_leading`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tracked formatting changes (`text:format-change` regions, ODF 1.3
§5.5.7, between `text:change-start`/`-end` marks) were dropped. The
text between the marks now gets a formatting revision with the change's
author and date. ODF records no old formatting, so the old formatting
is the text's own: the revision shows and can be accepted, and
rejecting it changes nothing (LibreOffice can't reject one after
reopening either). WordCraft's formatting revisions are written as
format changes around their text.
Fixed: changed regions were written with `text:id` only, but ODF 1.3
requires `xml:id` (which the change marks refer to), so any document
with tracked changes failed schema validation. Both are written now,
and a region named by `xml:id` alone is read.
A LibreOffice document with a tracked formatting change round-trips
pixel for pixel; documents with tracked insertions, deletions and
format changes validate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drop caps (`style:drop-cap`, ODF 1.3 §17.9) are read into WordCraft's
drop cap (lines and distance; one line is none) and written back as
the paragraph style's `style:drop-cap`. WordCraft drops one letter, so
a drop cap of several letters or a word opens as its first letter.
Fixed: the paragraph style key kept its child elements after a
separator, and the background, borders, contextual spacing and
right-to-left direction were added after the tab stops, so a paragraph
with its own tab stops wrote them into the tab stops' place instead of
onto the paragraph properties, and lost them. Attributes and child
elements are now gathered apart
(`tab_stops_and_other_paragraph_properties_together`).
A LibreOffice document with a drop cap round-trips pixel for pixel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A flat ODT holds the whole document in one `office:document` element
(ODF 1.3 Part 3 §2.1.2), its pictures inline as base64
`office:binary-data` and its formulas inline in their `draw:object`.
It is opened by taking it apart into a package's files (content,
styles, meta, settings; pictures and formulas as entries, each part's
root keeping the document's namespaces) and reading that package, and
saved by putting a package's files together: one set of automatic
styles, every part's namespaces declared once on the root, pictures and
formulas inline without the link attributes.
`.fodt` opens and saves from Open, Save As, the CLI and the web app.
LibreOffice's own flat documents round-trip as their packages do, and
the written files validate against the ODF 1.3 schema.
The parity doc's ODT fidelity is re-estimated at ~80%, and its status
line, which hadn't been updated since the Asian fonts change (a text
replacement that matched nothing), now lists every change since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Page number format: the page layout's `style:num-format`, and a page
number field's own (`PAGE \* Arabic`, `roman`, `ROMAN`,
`alphabetic`, `ALPHABETIC`), both ways.
- Page colour (the first page style's `fo:background-color`).
- Page borders: ODF's sit at the margin with the text inside their
padding; WordCraft measures a page border from the page edge and the
text margin from the edge, so the border's space is ODF's margin and
the margin takes in the border and padding, and back on save.
- Footnote and endnote numbering (`text:notes-configuration`: format,
first number, counted from 0 in ODF, restart, page foot or document
end); only what the document sets is written.
- Line numbering (`text:linenumbering-configuration`, on unless it says
otherwise, ODF 1.3 §19.845: interval, distance, restart on each page).
A LibreOffice document with all of these round-trips pixel for pixel;
WordCraft draws it 7,213 px from LibreOffice (24,025 with the border at
the page edge). The written styles validate against the ODF schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Fields that were read as plain text now open as Word's, so Update Field
recomputes them, and are written back as ODF's:
- dates and times in their date or time style (`number:date-style`,
`number:time-style`) ↔ `DATE`/`TIME \@ "picture"`: day, month (by
number or name), year, weekday, hours (12-hour with AM/PM), minutes,
seconds and the text between, both ways; data styles are written as
automatic styles and the `number` namespace declared;
- sequences numbering figures and tables (`text:sequence`) ↔ `SEQ` with
its number format; on save the sequences are declared
(`text:sequence-decls`); on open a bookmark named by the sequence's
reference name covers the caption's label and number ("Figure 1"),
what references to it show;
- chapter fields (`text:chapter`) ↔ `STYLEREF "Heading n"`, by name or
number (`\n`);
- reference marks as bookmarks, and references to them and to sequences
(`text:reference-ref`, `text:sequence-ref`) as `REF`/`PAGEREF`.
Captions renumber, references to them and chapter fields refresh after
Update Field (engine test `odt_fields.rs`). A LibreOffice document with
all of them round-trips 13 px apart with the same text; the written
content and styles validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A bookmark over text (`text:bookmark-start`…`text:bookmark-end`, in one
paragraph or across several) was read as a point at its start, and a
Word bookmark over text was written as a point: a reference to it then
updated to nothing. The flow model gets the end of a bookmark
(`Inline::AnchorEnd`, in flows asked for fields, as ODT's is), the
document gets the bookmark's start and end where they are, and the
writer writes a start and an end (a point, `text:bookmark`, when it
ends where it starts). A bookmark started twice, ended twice or ended
before it starts is kept once, as a point when it has no end after it.
References to bookmarks, LibreOffice's references to headings included,
now update to the bookmarked text (engine test `odt_fields.rs`, through
a save and reopen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tables of contents, tables of figures (`text:illustration-index`,
`text:table-index`), alphabetical indexes and bibliographies open as
WordCraft's: Word's `TOC` (levels from the source's
`text:outline-level`; `\c` and the caption name for figures), `INDEX`
and `BIBLIOGRAPHY` fields over their entries as last generated. With a
title the field goes in the title paragraph, as References › Table of
Contents inserts one; without, it spans the entries (new flow inlines
`FieldStart`/`FieldEnd`, mirroring the document's range fields). Update
Table then regenerates them from the headings (engine test
`odt_toc.rs`, titled and untitled, through a save and reopen).
LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles
are WordCraft's TOC 1–9, TOC Heading and Table of Figures, both ways,
also when a file names them without defining them. WordCraft's tables
are written as ODF indexes with their source and entry templates, so
LibreOffice updates them, the entries' links in the Index Link style.
User and object indexes are read as their paragraphs; index marks are
not read yet.
Found on the way, both shown by the table of contents' round trip:
- a style based on LibreOffice's generic Heading style (as its headings
and index headings are) lost that parent on save, and with it its
font and spacing;
- a paragraph's tab stops were measured from its own indent only, not
from an indent its style gives (LibreOffice's `TabsRelativeToIndent`),
and a named style's stops not from its indent at all.
A LibreOffice document with a table of contents, a table of figures and
an alphabetical index round-trips pixel for pixel (it was 29,717 px
apart and a page short). The parity doc's ODT fidelity is re-estimated
at ~72%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Polygons and polylines (`draw:points`), paths (`svg:d`) and regular
polygons and stars (`draw:corners`, `draw:concave`, `draw:sharpness`)
open as WordCraft freeforms in their view box (none: the box around
their points) instead of being skipped; circles (`draw:circle`, by size
or by centre and radius) open as ellipses.
The path data reader follows SVG path syntax: moves, lines, horizontal
and vertical lines, cubic and quadratic curves and their smooth forms,
and elliptical arcs (converted from endpoint to centre form), absolute
and relative, with repeated commands; curves are followed in 16 straight
steps, a subpath closed by `Z` is a closed path, and input is bounded
(numbers, points, paths, coordinates).
WordCraft's freeforms (ink included) are written as `draw:path` in a
view box of hundredths of a point (whole numbers, as ODF's view box is).
A LibreOffice document with a polygon, a polyline and a closed Bézier
curve (relative `c` and smooth `s` commands) round-trips 333 px apart,
and WordCraft draws it 529 px from LibreOffice; the written content
validates against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Graphic styles' arrowheads (`draw:marker-start`/`-end`), line dashes
(`draw:stroke-dash`) and gradient fills (`draw:gradient`), defined by
name in the styles, open as WordCraft's arrowheads, dash patterns and
gradients:
- dashes: up to two kinds of dash and one space, in line widths or
absolute lengths, matched to WordCraft's named dashes when equal;
- gradients: linear and axial along their angle (ODF's 0° runs top to
bottom, anticlockwise; WordCraft's left to right, clockwise; an angle
without a unit is tenths of a degree, as ODF 1.2 wrote it), radial,
ellipsoid, square and rectangular out from their centre (end colour in
the middle), with border and intensity.
They are written back as definitions in `styles.xml` (WordCraft's own
triangle arrowhead marker; equal definitions share a name; the header
writer's names are prefixed apart). Frames keep gradient fills too.
A line now keeps the direction it is drawn in (`svg:x1`/`y1` is its
start): one drawn leftwards or upwards is flipped that way, so its
arrowheads stay at their ends, and it is written from its start.
A LibreOffice document with arrowheads, dashes and gradients round-trips
215 px apart; WordCraft draws it 518 px from LibreOffice. Exported
styles.xml and content.xml validate against the ODF 1.3 schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Groups (`draw:g`) of shapes, text boxes and pictures open as one
WordCraft group instead of being skipped: anchored and wrapped as the
group is, over the box around its members, each member at its place in
it (from its `svg:x`/`svg:y`, its line ends or its `draw:transform`,
turned members turned); groups inside a group are flattened into it.
WordCraft's groups are saved as `draw:g`, members at their offsets with
no anchor of their own, nested groups nested (a canvas as a plain group).
A frame, shape or group whose styles state no `style:wrap` now has text
on both sides, as LibreOffice wraps it (ODF 1.3 §20.400 states no
default), not above and below only; `biggest` is read as the larger
side. A shape whose only paragraph is LibreOffice's empty `<text:p/>`
gets no empty text story.
A LibreOffice document with turned shapes and a group was 7,464 px apart
after a round trip; now 586, with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Shapes and pictures turned by `draw:transform` (rotate, translate and
scale, applied in turn to the object's own coordinates) open turned: the
file's anticlockwise turn about the object's top left corner, followed by
a move, becomes WordCraft's clockwise turn about the centre of the
unrotated box at its offset, as Word places them. They are written back
the same way, in line as well as floating. LibreOffice writes no
`svg:x`/`svg:y` for a turned shape, so before, a turned shape lost both
its turn and its position.
Custom shapes keep their mirroring (`draw:mirror-horizontal`/`-vertical`
on their geometry), and a turned line is written by its turned ends. The
flow keeps the placement of an in-line object that is turned or mirrored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Drawing shapes (`draw:rect`, `draw:ellipse`, `draw:line`, and
`draw:custom-shape` of a predefined geometry named by its
`draw:enhanced-geometry` `draw:type`) open as WordCraft shapes instead of
being skipped: size, placement (in line or floating, as frames are),
fill and line from the graphic style chain and the default graphic style
(LibreOffice's solid fill and hairline where nothing is stated), their
text, and a line's direction from its two ends. LibreOffice's type names
and its `ooxml-` preset names map onto WordCraft's kinds; a geometry
WordCraft has no shape for opens as a rectangle.
WordCraft's shapes are saved the same way: a custom shape by its type
alone (LibreOffice rebuilds the geometry from it), a line by its ends,
connectors as lines. A shape is saved with `draw:auto-grow-*` off unless
it resizes to fit its text, so LibreOffice keeps its size.
The flow model's text box becomes `Inline::Shape` with a kind; text
boxes are the `TextBox` kind and are written as frames, as before.
Not yet: shapes in groups, polygons, paths, rotation, arrowheads,
dashes, gradients.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A text box frame's line (colour and width), fill and padding are read
from its graphic style and that style's parents (LibreOffice's Frame
style included) instead of a fixed thin black line on white, and written
back. ODF's defaults (no line, fill or padding) apply where the styles
say nothing. A frame without a height, or with a `fo:min-height` on its
text box, opens as a box that resizes to fit its text and is saved with
a least height again. A LibreOffice text-box document was 1,117 px apart
after a round trip; now 736.
Layout: a floating text box that resizes to fit its text now takes its
text's height (drawn and wrapped around), as inline ones already did;
before, it kept its stored height (an ODT frame without one showed as a
1 pt line with the text spilling over the paragraph).
A formula sub-document's content.xml stays a bare `math:math` root, as
ODF 1.3 Part 3 §2.2.9 B requires (pinned by a test), and the reader now
skips a wrapping `office:math` some writers add instead of taking it for
the MathML root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The Asian and complex-script fonts (`style:font-name-asian`/`-complex`,
through their font-face declarations, or `style:font-family-*`), the
complex script's size, weight and posture, and the Asian and complex
languages (`style:language-*`, `style:country-*`) are read on runs and
from the default style, into the run's and the document's
character properties, and written back. Hebrew, Arabic and CJK text
lost its fonts on a save (an RTL test document: 2,043 px apart in
LibreOffice after a round trip, now 529).
A run's own complex-script weight or posture is written once, not also
copied from its Latin bold or italic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:writing-mode` `rl-tb` (or `rl`; ODF 1.3 §20.404) on paragraph
properties opens as a bidirectional paragraph, from automatic and custom
paragraph styles alike, and is written back on the paragraph's or the
custom style's properties. Right-to-left paragraphs opened left to right
and left-aligned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `draw:frame` holding a `draw:text-box` (ODF 1.3 §10.4.3) opens as a
WordCraft text box: a text box shape of the frame's size, in line or
floating as the frame is anchored and placed, whose story holds the
box's paragraphs (read as a note's are, with the paragraph and frame
around them put aside meanwhile, so a picture inside the box is a frame
of its own). The text was dropped on open.
Export writes each text box as a frame with a thin border and white fill
(`draw:fill` as well as `fo:background-color`: LibreOffice fills a frame
without `draw:fill` with its default blue). The module documentation now
lists what the reader and writer handle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice gives a heading style the outline's numbering unless the
style names no list style (`style:list-style-name=""`, as LibreOffice
writes its own). Our heading styles named none, so a heading whose level
the outline doesn't number still took that level's default indent: a
Word-made document's headings sat indented after a save. Heading styles
of unnumbered levels now name no list style; numbered ones stay
attached. Round-trip of that document: 3,302 px apart, now 1,121.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Contextual spacing (`style:contextual-spacing`, ODF 1.3 §20.255: no
space between paragraphs of one style) is read from paragraph, custom
and kind styles and written back, and the List Paragraph style is
written like any other, so bulleted items keep their spacing; they were
written in Standard and spread apart.
LibreOffice switches many layout behaviours by `settings.xml`, and a file
without them is laid out its own way. A file's own `settings.xml` is now
kept through a save; a document from elsewhere gets the switches
LibreOffice uses for Word files, as WordCraft lays text out as Word
does. Tab stops are written from the margin either way. A Word-made
document saved as ODT drew 38,652 px apart from LibreOffice's own ODT of
it; now 3,302, with the same line breaks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common text style other than LibreOffice's link, note-mark and
list-label styles (Emphasis, a user's own) opens as a document character
style with its display name, the custom character style it is based on
and its own formatting; the text in it names it, and only what the
spans' automatic styles add is direct formatting. Before, the style was
lost and its formatting copied onto the text. An id a paragraph style
already has gets "Char" added.
Export writes each custom character style the text uses, and those it is
based on, as common text styles; a span's automatic style is based on
its style and holds only the run's direct formatting. LibreOffice keeps
the style when it saves the file. Automatic styles of styles.xml are
never taken for custom styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each list level's indent and hanging (`fo:margin-left` and
`fo:text-indent` of `style:list-level-label-alignment`) and its label's
alignment (`fo:text-align` of `style:list-level-properties`, ODF 1.3
§17.19-17.20) are read into the document's list levels, for lists and
heading numbering, and written back. Every list was written 36 pt a
level with left-aligned numbers, so LibreOffice's right-aligned numbers
and closer bullets moved on a save (lists test document: 658 px apart
in LibreOffice after a round trip, now 8).
The flow label keeps the position in twentieths of a point, its prefix
and suffix now up to 8 bytes ("Chapter " still fits), so list
paragraphs stay as small as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A common paragraph style other than Standard, the paragraph kinds' and
the note texts' (LibreOffice's Text body, a user's own style) opens as a
document style with its display name, the style it is based on (a
custom parent, a kind's style or Normal) and its own formatting:
spacing, indents, line spacing, alignment, keep with next, borders,
background, tab stops and text formatting. Its paragraphs stay in it;
only what the automatic styles over it add is direct formatting. Before,
the style was lost and its formatting copied onto every paragraph.
Headers, footers, notes and comments are covered too.
Export writes each custom style the paragraphs use, and the custom
styles they are based on, as common styles named by an XML-safe form of
the style id and shown by the style's name; LibreOffice keeps them when
it saves the file. A custom style may not take a name the writer uses
for its own styles (Standard, Footnote, Endnote, the kinds'), so no
style is written twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each equation is written as an embedded formula object (ODF 1.3
§10.4.3): a frame in line with the text whose `draw:object` points at a
sub-document `Object N/` (declared in the manifest as a formula) whose
content.xml is the equation's MathML, with its linear text as an
annotation. The reader already opens LibreOffice's formula objects, so
equations round-trip. They were written as their linear text.
LibreOffice scales a formula to its frame, so the frame is sized from
the equation's structure (a box per node: fractions, scripts, radicals,
n-ary operators, delimiters, matrices) and centred on the line.
Checked in LibreOffice 24.2 with LibreOffice Math.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export wrapped a heading numbered through its style in a `text:list`
with WordCraft's default list style, so LibreOffice laid its number out
with that list's indents: level 2's "1.1." sat 54 pt in from the margin
(measured against LibreOffice's own file). A heading numbered only
through its style is now a plain `text:h`, numbered by the outline
style. The outline levels also get label alignment followed by a tab,
and the heading styles no longer state zero indents, which in
LibreOffice override the numbering's placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame anchored to a paragraph, character or page (`text:anchor-type`,
ODF 1.3 §19.11) opens as a floating picture: its graphic style's
`style:wrap` and `style:run-through` give the wrap (square on both
sides, left, right or the larger side; top and bottom; behind or in
front of text), `style:horizontal-pos`/`-rel` and `style:vertical-pos`/
`-rel` its alignment or, with `svg:x`/`svg:y`, its offset and what it is
relative to, and `fo:margin-*` its distance from the text. Every
picture used to open in line with the text.
Export writes floating pictures anchored the same way, placed by a
graphic style of their own; pictures in line stay `as-char`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Paragraph borders (`fo:border*`, ODF 1.3 §20.183; each side's
`fo:padding*` as its distance from the text) and background
(`fo:background-color`, `transparent` as none) are read from paragraph
styles, the nearest in the chain but the kind's style and Standard, and
written back on the paragraph's style. Only an empty paragraph with a
bottom border was kept before, as a horizontal rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:outline-style` (ODF 1.3 §16.34) numbers the headings of its
levels. On open its levels (format, prefix, suffix, levels shown, start)
become one list, each level linked to its heading style, and Heading
1-6 number their paragraphs through it, as Word's numbered headings do:
LibreOffice's "1. / 1.1." headings keep their numbers (they were lost).
Levels without a number stay unnumbered.
Export writes a `text:outline-style` from the numbering of the heading
styles; levels without one show no number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A `style:font-name` names a `style:font-face` in office:font-face-decls
(ODF 1.3 §16.23, §20.194); the font is that face's `svg:font-family`.
The reader took the face's name for the font, so LibreOffice's
`FreeSans1` opened as an unknown font "FreeSans1" instead of FreeSans.
Faces declared in content.xml and styles.xml both count; a name with no
face is still taken as the font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Cell borders (`fo:border*`: width, style, colour; ODF 1.3 §20.183) and
padding (`fo:padding*`) are read from table-cell styles. LibreOffice
gives an edge two cells share to one of them only (the left cell's
right side `none`, the right cell's left side drawing it); on open the
line goes to both cells, as WordCraft's cells each draw their own sides.
A table whose cells all say "no line" opens without a grid.
Export writes each cell's padding (its own, its table's or Word's
default) and its borders with each shared edge once, as LibreOffice
does (left and bottom, the first row's top, the last column's right), so
lines no longer double; cells used a fixed 4 pt padding and a grid even
in tables without lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On open, the file's own Heading 1-6, Title, Quotations and Preformatted
Text styles (named or displayed so, their whole chain but Standard)
give WordCraft's matching styles their font, size, bold, italic,
colour, spacing, indents, alignment and keep with next: a LibreOffice
heading is bold Liberation Sans, no longer WordCraft's blue heading.
`fo:font-size` percentages are read as a share of the inherited size.
On save, those styles are written from the document's own definitions
in full, instead of fixed ones, so a WordCraft heading looks the same in
LibreOffice. The styles test document now round-trips pixel-identically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Tab stops (`style:tab-stops`, ODF 1.3 §17.7-17.8) open with their
alignment (left, centre, right, decimal) and leader (dot, hyphen,
underline, middle dot), and are written back on the paragraph's style;
the default tab interval (`style:tab-stop-distance`) round-trips.
LibreOffice measures tabs from the paragraph's indent when its
`TabsRelativeToIndent` setting is on (its default); opening follows the
setting, and saved files carry a settings.xml that turns it off, so
positions run from the margin as in Word and WordCraft.
The flow model carries the tabs in `Para::extra` only for flows that
ask (FlowOpts::paras).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`style:text-properties` beyond bold, italic and colour now open and
save: underline styles (double, thick, dotted, dashes, wave, words only)
and colour, double strike, caps, small caps, hidden text (kept, hidden),
letter spacing, scale, text raised or lowered at full size (a position,
not superscript), outline, shadow, emboss, engrave and language.
A monospaced font keeps its own name; it was read as "code" and written
as Courier New. The default style's pair kerning (`style:letter-kerning`)
is read into the document defaults and written back, so lines break
where LibreOffice breaks them (round-trip renders of the test documents
are now pixel-identical in LibreOffice).
The flow model carries this in `Fmt::extra` only for flows that ask
(FlowOpts::chars); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Each level of a list style (ODF 1.3 §16.30-16.33) is read with its
number format (1, i, I, a, A, none), prefix and suffix, levels shown,
start value and bullet character, and written back. Each `text:list` is
a list of its own unless it continues the one before it
(`text:continue-numbering`) or the one it names (`text:continue-list`);
an item's `text:start-value` restarts from there. A list inside a note
or table cell leaves the list around it alone.
Before, consecutive lists merged into one: three LibreOffice lists
numbered 1., i. and A. opened as one decimal list 1-7.
The flow model's ListInfo carries a list number and a compact label
(prefix and suffix up to 12 bytes, so "Chapter %1" fits) only for flows
that ask for them (FlowOpts::lists); the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`office:annotation` (ODF 1.3 §14.1) opens as a document comment with its
author, date, initials and text; `office:name` and
`office:annotation-end` (§14.2) give its range, across paragraphs too.
LibreOffice's resolved state (`loext:resolved`) and reply threads
(`loext:parent-name`) are kept: a reply is a comment of its parent with
no range of its own. An end with no comment is dropped, and a comment
whose end never comes covers nothing. LibreOffice's all-zero date is no
date. Before, comments were dropped on open.
Export writes each comment named, with its end where its range ends,
and its replies after it naming it; LibreOffice keeps the threads when
it saves the file again. Truncated XML no longer loses the paragraph a
note or comment sits in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
`text:note` (ODF 1.3 §6.3) opens as a note story built as Word's: its
paragraphs in Footnote Text or Endnote Text, the first starting with the
note's own mark, the reference in the text in the reference style, and a
`text:label` custom mark kept. The paragraph a note sits in (with its
spans, list and table cell) is put aside while the note's content is
read, and goes on after it. Notes inside notes, in deleted text or in
frames are left out. Before, notes were dropped on open.
Export writes each note with a citation numbered as LibreOffice shows
it (footnotes 1, 2, 3; endnotes i, ii, iii) or its custom mark, its
paragraphs in Footnote or Endnote styles defined in styles.xml. An
insertion open around a note ends before it and goes on after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Export writes each section's headers and footers into its master page
(style:header, -left for even pages, -first for a different first page;
an empty element where Word shows nothing), with their automatic styles
in styles.xml under names of their own. Word's page-edge header and
footer distances become ODF's page margin plus header/footer band
height, and import converts back, so headers sit where LibreOffice
draws them.
Text fields (ODF 1.3 §7: page number and count, date, time, document
information, bookmark references) open as WordCraft fields with their
shown text as the result, and are written back as ODF fields. Fixed
dates and fields without a Word equivalent stay text; Word fields ODF
lacks are written as their result. The flow model carries fields only
when asked (FlowOpts), so the other formats are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Read `fo:margin-*`, `fo:text-indent`, `fo:line-height` (percent, length,
normal), `style:line-height-at-least` and `style:line-spacing` (leading,
as a proportional approximation) from automatic and common paragraph
styles (ODF 1.3 §20.204-209, §20.225, §20.317-318), and write them back
from each paragraph's direct formatting.
The default paragraph style and Standard give the document defaults, on
ODF's initial values: an ODT paragraph without spacing has none, not
Word's 8 pt after and 1.15 lines. Export writes the document's Normal
spacing as the default paragraph style, so the copy reopens the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A pStyle id the style sheet doesn't define gave an empty style chain, so the
paragraph got the document defaults alone. Word formats it as Normal: a Pandoc
footer naming an undefined Footer style was taller than Word's and pushed the
last paragraph of the page over.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A hyperlinked TOC wrote each entry's page number as plain text inside the
link. Word writes it as a PAGEREF _Toc… \h field, so Update Field in Word
moves the number with its heading; with plain text the numbers stay what
they were when the file was saved.
Entries that link to a bookmark now end with that field (result: the page
number). Entries without a link and \n levels are unchanged. The DOCX span
test looks for the TOC's own end, the last one, since PAGEREF fields now end
inside it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Treat <!--> and <!---> as empty comments in HTML import
Found while testing crates/formats/src/html/lexer.rs:70-73 (Lexer::next, `<!--` branch)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs: file-format-parity HTML row for empty comments (#5040)
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Keep with next asked for two lines of the paragraph after a kept heading under widow/orphan
control. A three-line paragraph cannot split at all there (two lines and a widow, or an orphan
and two), so it moved over whole and left the heading alone at the foot of the page. Word moves
the heading with it: ask for all three lines in that case.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>