The file's office:forms keeps its own head and each form:form with what
WordCraft doesn't model; controls are merged with the file's own. An
index whose generated entries nobody edited is written as read, with
its source settings and entry templates; an edited one is written from
the model. The no-edit gate saves nine samples of our own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The standard (§18.3.1, since ODF 1.2) reads an angle without a unit in
degrees. WordCraft read unitless gradient angles in tenths of a degree in
every file; in ODF 1.3 files it now follows the standard.
LibreOffice and OpenOffice wrote gradient and hatch angles in tenths of a
degree in their files before ODF 1.3 and still read them so; read by the
standard, every older LibreOffice document's gradients would turn the
wrong way. So in files older than 1.3 they are still read in tenths: a
deviation from the standard, out of necessity, now listed in
docs/odf-interpretation.md (a new rule and section for such deviations).
WordCraft always writes these angles with a unit.
The ODF 1.2 -> 1.3 changes (ODF 1.3 Part 3 Appendix G and their OASIS
issues) were checked: none other gives a value written for 1.2 another
meaning; the new defaults match how LibreOffice already read them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More of what WordCraft doesn't change is written back as it was read:
tables; the document properties it doesn't model (template, statistics,
editing time, printing); the declarations opening the text and the
document's scripts; page layouts and master pages while the page setup
is unchanged, each section starting with the master page it started
with; named sections around unedited paragraphs. Package files and
folders kept markup names (a style's fill picture, a header's embedded
object) are written with it, with their manifest entries.
Files are saved as ODF 1.3; markup kept from an older file is brought to
1.3's meaning (a gradient's or hatch's unitless angle was tenths of a
degree). WordCraft's own drawings state style:flow-with-text, which the
standard leaves open.
odt::tests_more::lossless saves seven samples of our own without edits
and fails on anything lost (nothing is allowed), and checks that edits
keep the saved parts well-formed.
Over the 133-file corpus the round-trip difference falls from 191k to
51k pixels; 95 files come back pixel-identical (62 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A placeholder test used the label <Title>, which LibreOffice's letter
templates also use; it now uses an invented one. The kept-markup tests
say that every sample (and any sample file the no-edit loss gate adds)
is written by us, never taken from someone else's files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph nothing changed since it was read (by a fingerprint of how it
was modelled) is written with its own markup, unknown elements and
attributes included, together with the automatic styles, common styles,
font faces, namespaces and package files it names. While no style or list
definition changed, styles.xml's office:styles is the file's own (unused
list styles, note and line numbering configuration, attributes WordCraft
doesn't read); drawing definitions and the default graphic style stay
WordCraft's, as drawings are written from the model.
Paragraphs holding drawings, comments, tracked changes or ranged marks,
or starting a page, are written from the model, as is everything edited.
Kept markup lives in memory only (wordcraft_doc::kept, 64 MB at most) and
is never part of the document's content.
Over the 133-file corpus the round-trip difference in LibreOffice falls
from 528k to 191k pixels; 62 files come back pixel-identical (46 before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice reads an author field without text:fixed as fixed (the
standard says not fixed) and writes false itself; an empty author field
stayed empty after a save instead of showing the author. Sender fields
are written as LibreOffice writes them, without the attribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The default paragraph style's text colour (fo:color) is read into the
document's default and written back: Google Docs' grey résumé came back
black. What a covered table cell holds (not shown, §9.1.5) is skipped;
it went before the table as empty paragraphs, pushing ONLYOFFICE's table
down the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.
LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Kerning turned on or off span by span (style:letter-kerning) is read and
written where it differs from the document's default; without it
LibreOffice set ONLYOFFICE's text wider and every page broke elsewhere
after a save. A note reference keeps its own formatting (a span around
text:note), and a note starting with a tab keeps it: only the space
WordCraft puts after the mark is left out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A fixed date or time (text:fixed) opens as a locked field and is written
back fixed, with the value its text shows under its picture
(text:date-value / text:time-value), which LibreOffice needs to keep it.
One whose text can't be read back is written as text. Sender fields open
as SENDER fields and are written back as text:sender-*.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
User fields (text:user-field-decl, text:user-field-get: LibreOffice's
report templates, 58 documents of the corpus) were read as custom
document properties and written back as meta:user-defined properties
with text:user-defined fields: the look and values stayed, the kind
didn't. Custom properties now remember being user fields
(CustomProp::odf_user_field) and are written back as
text:user-field-decls with their values and text:user-field-get fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by a loss check comparing what a file uses with what a WordCraft
save writes back (over the 133-document local corpus):
- Every font a file uses is declared (office:font-face-decls, §16.21:
family, generic family from its name, pitch) and named where it is
used (style:font-name and its Asian and complex forms beside the
fo:/style: font families), so a reader without the font picks a like
one. Lost in 132 documents.
- meta.xml keeps who changed the document last (dc:creator), when
(dc:date) and how often it was saved (meta:editing-cycles), read into
and written from CoreProps. Lost in 120.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with Google Docs' and ONLYOFFICE's ODT files (now in the local
corpus):
- A heading's look comes from its style, its outline level from its
element: a text:h without text:outline-level (level 1, §19.850.4) in a
style based on Heading 2 looks as Heading 2, as LibreOffice draws it,
and keeps outline level 1 (it became Heading 1). The level is carried
apart from the style (ParaProps::outline_level through the flow) and
written back as text:outline-level.
- Widow and orphan control is stated in the default paragraph style
(fo:widows/fo:orphans 2) when Normal has it: LibreOffice reads none
unsaid as off, so pages broke differently.
Google's 2011 Fundamentals export now round-trips with its 88 pages and
its text (1.27M -> 139k differing pixels).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by comparing WordCraft's own documents in LibreOffice and
bisecting settings.xml (observed only, black-box):
- The settings set's name, ooo:configuration-settings, is a qualified
name: without xmlns:ooo declared LibreOffice ignored every setting
WordCraft wrote (its Word layout rules, tab measuring).
- A set LibreOffice reads must be complete: a setting missing from it
gets its old pre-OpenOffice value, which changes the layout. WordCraft
now writes LibreOffice's current layout defaults (as a new LibreOffice
document has them), overlaid with Word's layout rules for documents
WordCraft lays out as Word does (not for ones opened from ODF), and
its own choices.
- How the space between paragraphs adds up follows a file's
AddParaTableSpacing (false: the larger, as Word); unstated, the ODF
profile's new sumParagraphSpacing (WordCraft: the larger, LibreOffice:
the sum). WordCraft writes it as it lays the document out.
WordCraft-origin documents in LibreOffice: 142k -> 80k differing pixels
over 42 documents; the LibreOffice corpus: no regressions, 40k -> 39.6k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice's elegant agenda puts a small gradient picture behind cells
(§17.3 style:background-image in a table-cell style). Cells can now
carry a background picture (CellProps::picture: media, own size,
repeat none/tile/stretch, alignment), which layout draws over the
cell's shading and under its text, clipped to the cell (Placed::Clip of
Placed::Image; tagged as an artifact in PDF). The ODT reader reads it
(style:repeat, style:position, the picture from the package) and the
writer writes it back with the picture in the package (once per
picture).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The padding inside an ODT page border (§20.210) was capped at Word's
31 pt border spacing when opening, so the text of WordCraft's own page
borders (24 pt from the edge, 1 inch margins: 45 pt of padding) moved
towards the border on reopening. In ODF the padding is the distance to
the text, any length on the page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
WordCraft's watermark was left out of ODT files. It is now written as
LibreOffice and Word write theirs: a fontwork custom shape
(draw:text-path, draw:type fontwork-plain-text) named
PowerPlusWaterMarkObject, behind the text and centred on the page, in
every header (a header made for it alone fits inside the top margin, so
the text doesn't move). It doesn't grow to its text (draw:auto-grow-*
false), which LibreOffice needs to stretch the text to it. Such a shape
in a header, from LibreOffice or WordCraft, opens as the document's
watermark (text, colour, see-through, turn, size), not as a shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).
- Table styles: ODF has no table styles of Word's kind, so each cell now
states what the style gives it (fills, its borders, bold header text,
its paragraph spacing), under the paragraph's own styles as layout
applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
table_char_patch). The style's border mapping moved to
styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
single spacing changed their text.
- Columned sections say whether their columns end balanced
(text:dont-balance-text-columns, §20.428, read as its name says; its
value descriptions contradict it). Unstated, the ODF profile decides
(balanceSectionColumns: LibreOffice balances, WordCraft as Word).
SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
an invisible frame keeping the graphic's box), which LibreOffice draws
as WordCraft does.
LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODF 1.3 §20.206–20.209 define page margins by referring to XSL's
(§7.10), without saying whether XSL's initial value 0 comes with the
reference; LibreOffice uses 2 cm. The margin a page layout leaves out
is now the profile's `pageMargin`: WordCraft 0 (XSL's), LibreOffice
2 cm, or custom (File › Options › Advanced › OpenDocument files,
`file.options` `odf.pageMargin`). A stated margin is the file's under
every preset. With the LibreOffice preset, LibreOffice's CV, sans-serif
business letter and one-page résumé templates match LibreOffice again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- File › Options › Advanced › OpenDocument files: the preset is a
drop-down like Measurement units and Region; shape line width is in
points (a 0.25 pt hairline showed as 0.00").
- WordCraft's preset keeps a header's or footer's stated spacing when
its style doesn't say whether it is dynamic: with no height stated,
growing into it always dropped the spacing the file states. Custom
profiles can still grow into it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).
Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.
- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
ROADMAP updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Form controls:
- Placement: a control hanging from the baseline (as LibreOffice places
those it makes) was saved with no placement. LibreOffice then puts it
at the line's top, so every control row shrank. Its raise is now kept
(`ContentControl::form_raise`, read as in-line frames are) and
written, standing on the baseline when it had none.
- Multi-line fields: these were saved without LibreOffice's `MultiLine`
property, so they showed one centred line. They now carry it.
- Text fields get LibreOffice's flat border.
- Also: `pt()` no longer writes "-0pt".
Custom shapes:
- A shape drawn from its own geometry was saved as WordCraft's path of
it, and a preset WordCraft draws as a rectangle (a cloud) was saved as
a rectangle. The geometry as read (type, path, equations, handles) is
now kept (`ShapeExtra::odf_geometry`) and written back, mirrored as
the shape now is.
- `shape.change` and the ink-to-shape conversion drop the kept geometry,
since the shape is then another kind. `ShapeExtra` holds it as an
`Arc<String>` to keep `Placed::Shape` small.
- The reader's `end` hands skipped elements to `end_skipped`.
Results:
- The forms document round-trips pixel-identical (2,949 pixels before),
with its text the same.
- The shape presets fall from 2,467 to 1,220. The rest is LibreOffice's
hairline outline, which WordCraft keeps as 0.5 pt.
- Across the corpus the round-trip difference falls from 45k to 41k
pixels. 42 of 125 documents round-trip pixel-identical, 92 with their
text the same, and none gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's style templates.
- An in-line frame placed against the text (`style:vertical-rel` "text"
or "char", with `middle`, `top` or `bottom`) stood on the baseline. It
is now raised or lowered against the text's height, from the size of
the text it sits in. Measured in LibreOffice, `middle` centres the
frame on the text: its middle half the ascent less the descent above
the baseline. Placement against the line still stands on the
baseline.
- An in-line picture or chart was saved with no placement style. In
LibreOffice that puts it at the line's top (measured: the title beside
the Finances logo moved up 19 pt). It is now saved standing on the
baseline ("top" to "baseline"), as WordCraft and Word show it.
Two templates' round-trip differences fall from 1,874 and 1,433 pixels
to 521 and 586. Across the corpus the difference falls from 47k to 45k,
and no document gets worse.
Left in these templates:
- template instantiation: LibreOffice refreshes a template's fixed dates
and fills its sender fields from the user's data when opening it;
- a first-page footer's frame placed against the page content;
- a page background picture centred in the margins rather than on the
page.
gaps.md row 19 lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Charts: on save, WordCraft wrote each chart anew from its chart model
(type, data, title, legend, labels), losing the look it doesn't model:
fonts and sizes, axis and grid lines.
- The chart object as read is now kept with the chart (`Chart::source`
in the flow model, `Graphic::source` in the document).
- `chart::redraw` keeps it when the chart is drawn again on open or
resize.
- The ODT writer writes it back whole while it still reads as the same
chart. An edited chart is written from the model, as before; DOCX
still writes charts from their model.
- The chart documents now round-trip pixel-identical (chartcol: 5,917
pixels to 0).
LibreOffice's serif business letter (4,223 pixels to 116):
- A text frame outlined by a drawn stroke (`draw:stroke`, a 0 width a
hairline) and no `fo:border` lost its outline. It is now the frame's
line; an explicit "none" border still wins.
- A header or footer whose style states no height, or that has no style,
was 0 tall. LibreOffice makes it 0.5 cm (measured by varying the
height), so the text had moved up 1.5 pt.
Across the corpus the round-trip difference falls from 60k to 47k
pixels, and 41 of 125 documents round-trip pixel-identical. No document
gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's agenda templates. Saving wrote a table's first
row as `table:table-header-rows`, which LibreOffice repeats at the top
of each page, so a table crossing a page showed its first row twice
("<bring>" twice). The cause: the flow model marks a first row as a
header when the table style's look has a header row, which Word's look
has by default. That only affects the style's formatting, and HTML and
Markdown still rely on it for heading cells.
- The flow table now carries how many rows repeat
(`TableExtra::header_rows`, Word's Repeat as header row).
- The ODT writer writes only those rows as header rows.
- That extra block is now always kept for ODT flows, where it had been
dropped from tables without an alignment, width or row height.
All 16 agenda templates now round-trip with their text the same; the
modern and simple ones fall from about 5,200 differing pixels to 145.
Across the corpus, documents with the same text after a round trip rise
from 75 to 91 of 125, and the round-trip difference falls from 82k to
60k pixels.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's report templates and one-page résumé.
Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
without a border; measured, 0.5 in of padding and no border moves the
text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
`Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
setting, now renamed `Settings::odf_layout` to cover all of
LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
Word documents are unchanged.
Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
and sections) were made equal. They now keep their widths and are
written back as shares. The writer leaves out `fo:column-gap` beside
them, because LibreOffice ignores a page's column widths when a gap is
stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
with Word's column break, written back on the paragraph.
The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document gets worse.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.
- A new document setting, `Settings::object_lines_fit_objects`, makes
the layout size such lines by the pictures alone. The ODT reader turns
it on, so other documents keep Word's layout. An equation or any text
on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
Letter page with 1 inch margins. It now uses the region's paper with
LibreOffice's 2 cm margins.
WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's elegant agenda:
- Paragraph size: a paragraph holding only pictures lost its own font
size, since the flow model's picture runs carry no formatting, so its
line grew. Such a paragraph (`Para::textless`) now keeps its formatting
as the paragraph mark's, which its pictures' runs take too. Read and
written as for empty paragraphs.
- Raise: an in-line frame's raise was dropped when a parent style gave
it a relation no in-line frame has (the Graphics style's
`style:vertical-rel="paragraph"`). It is now measured from the baseline,
as LibreOffice reads it.
The agenda's round-trip difference falls from 6,542 to 1,331 pixels, and
two business fax templates' from about 5,300 to 270 and 350. Across the
corpus it falls from 185k to 155k. No document changes page count.
WordCraft's own rendering of a picture-only line is still taller than
LibreOffice's, a layout difference left for later.
gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found with LibreOffice's report templates.
Tables:
- A table filling the margins (`table:align="margins"`) but wider than
them ran off the page. It is now fitted to the text width, its columns
in proportion. Checked in LibreOffice, which fits such a table with or
without relative column widths; the old test's claim otherwise is
corrected.
- A table's own space above and below (`fo:margin-top` and
`fo:margin-bottom` on the table) was dropped. It is now read into new
`TableProps::space_before`/`space_after`, laid out (added to the
spacing of the paragraphs around it, as LibreOffice does) and written
back.
Headers and footers:
- A header taller than its band lost its spacing from the body, moving
the text up. LibreOffice keeps the spacing below the content unless it
is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
`SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
past a tall header or footer, and the writer writes it back. Word keeps
no gap, so DOCX documents are unchanged.
Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A frame that doesn't state its anchor or position now takes them from
its graphic style chain (ODF lets `style:graphic-properties` carry
`text:anchor-type`, `svg:x` and `svg:y`).
LibreOffice's business card template relies on this:
- its Labels style holds the card in line;
- its Graphics style puts the credit at the top of the page.
WordCraft had floated the card, so a save scrambled the layout and added
a page.
Across the 125-document corpus, no document now changes page count
through a save (3 did), and the round-trip pixel difference falls from
333k to 241k. gaps.md row 19 and the ROADMAP log are updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Saving LibreOffice's letter and fax templates added a page and moved
their logos. Fixes:
- Default font size: the writer always wrote the default style's size as
12 pt. It now writes Normal's size (11 pt in these letters).
- Page-anchored frames: a frame placed "from the paragraph" was moved by
the page margin. A page has no paragraph, so it is now placed from the
page, and "from the paragraph's content" from the page's text area, as
LibreOffice places it.
- Text box styles: a custom style used only by paragraphs in text boxes
(the fax's centred "Frame contents") was dropped. The style pass now
walks text boxes and groups.
- Empty paragraphs: an empty paragraph's own font size (its height) was
dropped. It is now kept as the paragraph mark's formatting, a new
field in the shared flow model that the ODT reader and writer use.
The marine, fax and bottle letters keep one page after a save. Over the
125-document corpus the round-trip pixel difference falls from 408k to
333k, and the business letters' differences roughly halve.
Also moved a doc comment that 48c60a5 left on the wrong function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT:
- turned characters;
- conditional text;
- control looks;
- embedded spreadsheets' contents;
- dates in non-US locales.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- The document's default font size (`fo:font-size` in
`style:default-style`, or the Standard style's: the document's
defaults) was ignored, leaving 12 pt; LibreOffice's letters are
10 pt. It is now the document's default. A named style with no parent
keeps the default style's size, as ODF inherits it.
- A date style ordered by the locale (`number:automatic-order`) showed
its parts as listed: "2026November2". It now shows in US English
order: "November 2, 2026", or "11/2/2026" with a numeric month, as
LibreOffice shows it here.
WordCraft's own rendering of LibreOffice's letter templates comes much
closer: one letter goes from 11,377 to 6,237 differing pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A page layout's background picture (`style:background-image`, §17.3:
LibreOffice's report templates' world map) was dropped. It is now
WordCraft's page picture (Design › Page Color › Fill Effects) and is
written back with its picture:
- repeated: tiled;
- stretched: stretched;
- not repeated: centred at its own size, a new fill that the screen
renderer and PDF export draw in the middle of the page. DOCX writes
it stretched, as VML has no centred fill.
LibreOffice's report template with the map goes from 11,719 to 3,602
differing pixels, and shows the map after a save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- A table starting a page (`fo:break-before="page"` on the table,
LibreOffice's agenda minutes) didn't. Its first paragraph now has a
page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
now moves the whole table to the next page, as Word does. It was
ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
LibreOffice's columns never fit their content.
LibreOffice's 6orange agenda now round-trips with the same text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
`w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
disc the dot, a dot below the under-dot.
Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
already draws nothing for SET, so its result stays as it was, as
`ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- ODT variables (`text:variable-set`, `text:variable-get`, §7.4) were
read as their text. A set is now Word's SET field, followed by a REF
when it is displayed; a get is a REF. They are written back declared
(`text:variable-decls`), the sets hidden with a get after them.
- The engine now evaluates SET (ECMA-376 §17.16.5.58). It shows
nothing and gives its name a value. REF to a name no bookmark has
shows that value; a REF before the SET shows the last value set, as
Word's bookmark does after an update. This works for Word documents
too.
- Custom character styles now keep what they turn off under a parent
that turns it on (`fo:font-weight="normal"`…), as paragraph styles
already do.
A LibreOffice document with variables round-trips with the same text,
and the saved file validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
gaps.md row 19 now lists what's left in ODT after the template work:
- emphasis marks and turned characters, which have no WordCraft model;
- variable fields;
- control looks;
- embedded spreadsheets' contents;
- tables wider than the margins;
- what a character style turns off.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More from LibreOffice's bundled templates:
- WMF and EMF pictures (LibreOffice's fax and letter logos) were
dropped. They now open as pictures, at their own size (a placeable
WMF's or an EMF's frame) when the frame gives none, and are saved
with their media types.
- A floating picture inside a text box, saved by WordCraft, didn't
show in LibreOffice. Picture frames' styles now derive from the
Graphics style (declared, like Frame), which LibreOffice needs to
show it.
- A named paragraph style turning off its parent's bold (agenda
dates under bold titles) showed bold, both in WordCraft and in
LibreOffice after a save. What a style turns off (bold, italic,
underline, strike-through) is now kept and written.
- Columns' shares (`style:rel-column-width`) are now written only for
tables filling the margins. LibreOffice lays out an aligned table
oddly with them: WordCraft's sample table lost its proportions.
- Drawings waiting for a paragraph now join only a paragraph of the
text itself, never one in a text box or note.
With the harness's paper setting fixed (it had made LibreOffice draw
unsized pages as Letter), most of the 78 templates compare much closer
page by page. The CV template and the contour sample are now
pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
More fixes from LibreOffice's bundled templates:
- A page margin the file leaves unsaid was 1 in, and a page layout with
no properties at all was ignored. Both now take LibreOffice's
defaults: 2 cm margins (observed black-box). LibreOffice's CV template
now lays out as LibreOffice draws it.
- Drawings anchored outside any paragraph (LibreOffice's
page-anchored frames before the first paragraph) made a paragraph of
their own. That paragraph started the document on the default page
style rather than the one the first paragraph asks for, which put the
running header on a letter's first page and pushed the letter onto
two pages. They now join the next paragraph; before a table or at the
end, they still get a paragraph of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
LibreOffice ships 78 text templates (letters, faxes, agendas, reports,
CVs). Round-tripping them through WordCraft exposed these problems:
- Placeholders (`text:placeholder`, "<Recipient's name>") were
printed text. They now open as plain-text content controls showing
them, so a click selects one and typing replaces it. Their
description becomes the control's title. They are written back as
placeholders of their type while they still show; what was typed is
written as text.
- A fixed date with neither a value nor text was lost. It is now a date
field.
- `text:author-name` opens as USERNAME, which the engine left empty.
USERNAME and USERINITIALS now show the user's name and initials
from File › Options; a quoted text after the name is shown instead.
- A "First Page" master followed by another (`style:next-style-name`)
put the running header on the first page. It is now a section with a
different first page: its own (empty) header on page 1, the next
master's after that.
- A page layout without a size was Letter. It is now the default
page's, the user's region's, as LibreOffice uses its default paper.
ODT import takes the regional page like the other importers.
- Style-only templates (no `content.xml`) failed to open. They now
open empty with their styles.
- Table columns are now written with their share of the table
(`style:rel-column-width`) too. A table filling the margins of a
narrower page keeps its proportions in LibreOffice.
All 78 templates now open and round-trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Hidden form controls (`form:hidden`: a name and value the form sends,
not shown) were dropped. They are now kept verbatim, with their
properties, and written back into the form with ids of their own.
- OpenDocument text templates (`.ott`) weren't opened. They now open
as documents, and save as templates: the template's media type goes
in `mimetype` and in the manifest's root entry (ODF 1.3 Part 2 §3.3).
This works with a password too. `.ott` is listed wherever `.odt` is:
open and save formats, file dialogs, export, the web app and its
media type.
A LibreOffice template round-trips with the same text. 300 fuzzed
documents exercising custom shapes, cell formulas, kept parts, turned
text and forms caused no crashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
After custom shapes, cell formulas, kept package parts, turned text,
and control sizes and buttons: ODT is ~90% in file-format-parity (20–35
h left). gaps.md row 19 lists what's left in ODT:
- emphasis marks;
- turned characters;
- conditional and variable fields;
- hidden controls and control looks;
- embedded spreadsheets' contents.
The ROADMAP progress log has the entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fix. A button kept verbatim was still written as the button
after its content control was made another kind (a check box, a list).
The kept form XML is now used only while the control is still the
plain text control it was read as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
On save, form controls were sized to their text. Buttons and other
controls WordCraft has no kind for were dropped.
- Sizes: each control keeps the size of its `draw:control`
(`svg:width`, `svg:height`), stored on the content control.
- Buttons, image buttons, labels, file and value-range controls, frames
and generic controls now show their label, locked, as a content
control. Their form XML is kept verbatim and written back as it was,
so LibreOffice's button round-trips as a button.
- A kept control that names a namespace WordCraft doesn't declare is
left out. So is one longer than 64 KiB.
- The `ooo`, `oooc`, `script` and `dom` prefixes that LibreOffice's
controls use are declared, so the content validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Text turned down or up opened across. This covered table cells and text
boxes (`style:writing-mode` §20.404, and LibreOffice's own
`loext:writing-mode="bt-lr"` for upward text).
- Table cells now open and save with WordCraft's cell text direction.
- Text boxes now open and save with their text direction.
LibreOffice's turned cells render as LibreOffice draws them. LibreOffice
reads WordCraft's turned text boxes back identically.
Turned characters (`style:text-rotation-angle`) still open upright:
WordCraft has no turned characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Package parts WordCraft doesn't read were dropped on save. These
include LibreOffice Basic macros and dialogs (`Basic/`, `Dialogs/`),
`manifest.rdf` and other applications' files. They are now kept as
they are and written back with their manifest entries. LibreOffice
finds a round-tripped document's macro intact; WordCraft never runs
macros.
Dropped on save: the old thumbnail, and digital signatures
(`META-INF/*signatures*.xml`), because they sign the package as it
was. Also dropped: paths that climb out of the package, and paths
WordCraft writes itself (pictures, embedded objects).
Kept parts are bounded: 1000 files, 64 MiB in all. They survive
password protection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
ODT cell formulas (`table:formula`) were lost, and only their value was
kept as text.
- Reading: LibreOffice's formulas, in its own syntax
(`ooow:sum <A1:A3>`, `<B1>*<B2>+<B3>`, `mean`, `min`, `max`,
`product`, `abs`, `sign`, `count`, `add`/`sub`/`mul`/`div`/`pow`, lists
joined by `|`), open as Word formula fields (`=SUM(A1:A3)`) showing
the cell's value. Update Field recalculates them.
- Writing: formula fields in cells are written back as cell formulas
with their value. ABOVE, LEFT, BELOW and RIGHT become the cells they
name.
- Cells holding a plain number are written as numbers, so LibreOffice
calculates with them. The `ooow` prefix is declared; without it,
LibreOffice reports "Expression is faulty".
A formula one side can't say keeps its value as text. A LibreOffice
table of sums round-trips with the same text, and the saved file
validates against the schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Review fixes to the custom shape geometry:
- An equation that couldn't be worked out wasn't remembered. Equations
each referring twice to the next, ending in one that refers to
itself, took about 2^64 steps. Each equation is now worked out once,
failures and those being worked out included.
- Points far outside the view box (1e300) became infinite as f32. They
are now clamped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC