Commit Graph
2560 Commits
Author SHA1 Message Date
Claude fdbc3322b3 ODT: unedited tables, pages, sections and properties written as read; no-edit loss gate
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
2026-11-03 13:24:09 +00:00
Claude c73e9e0e87 ODT tests: only samples of our own
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
2026-11-03 11:06:26 +00:00
Claude a6ba7321db ODT: what isn't edited is written back as it was read
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
2026-11-03 10:39:40 +00:00
Claude 1eff715f10 ODT: author fields that follow the document say text:fixed="false"
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
2026-11-03 08:44:28 +00:00
Claude ca2cf4e0fd ODT: default text colour; covered cells' content stays out of the body
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
2026-11-03 07:02:10 +00:00
Claude 6f6559aacb ODT: widow control as a profile case; tabs measured from the indent kept
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
2026-11-03 06:18:46 +00:00
Claude faac2c42bd ODT: span kerning, note reference formatting, notes starting with a tab
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
2026-11-03 05:33:13 +00:00
Claude 08dd0906ca ODT: fixed fields as locked fields with their values; sender fields
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
2026-11-03 04:40:16 +00:00
Claude 7160f05014 M5 #19: ODT user fields stay user fields
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
2026-11-03 03:54:32 +00:00
Claude 6601aeaf09 M5 #19: ODT font declarations and change metadata kept
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
2026-11-03 03:32:04 +00:00
Claude fdbe410704 M5 #19: ODT headings keep their style's look; widow control stated
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
2026-11-03 01:58:04 +00:00
Claude 75ee5e6a43 M5 #19: docs: known ODT layout differences between WordCraft and LibreOffice
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 00:51:04 +00:00
Claude 48ff99e52c M5 #19: LibreOffice reads WordCraft's ODT settings; paragraph spacing setting
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
2026-11-03 00:49:05 +00:00
Claude e635c8bd96 M5 #19: ODT cell background pictures
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
2026-11-03 00:10:57 +00:00
Claude cef4338947 M5 #19: ODT page border padding beyond 31 pt kept
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
2026-11-02 23:45:28 +00:00
Claude a330e4c503 M5 #19: ODT watermarks written and read
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
2026-11-02 23:36:02 +00:00
Claude 5063000781 M5 #19: ODT keeps table styles, SmartArt and section column balance
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
2026-11-02 23:24:24 +00:00
Claude 7b25253286 M5 #19: unstated page margins are an ODF interpretation setting
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
2026-11-02 22:31:49 +00:00
Claude ffa309c266 M5 #19: OpenDocument preset as a drop-down; header spacing kept by default
- 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
2026-11-02 21:43:26 +00:00
Claude 8d5485c8bd M5 #19: ODF interpretation profile (WordCraft / LibreOffice / custom)
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
2026-11-02 20:54:05 +00:00
Claude bc74bc23f5 Gap 19: ODT form controls keep their place and lines; custom shapes their geometry
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
2026-11-02 19:49:57 +00:00
Claude 7cdc8d85a4 Gap 19: ODT in-line frames placed against the text; saved on the baseline
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
2026-11-02 18:39:08 +00:00
Claude 90898f454b Gap 19: ODT charts keep their look; frame outlines; default header height
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
2026-11-02 17:11:20 +00:00
Claude 807fe91724 Gap 19: ODT tables keep header rows only where they repeat
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
2026-11-02 16:11:20 +00:00
Claude be2666da15 Gap 19: ODT padding without a border, unequal columns, column breaks
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
2026-11-02 15:39:32 +00:00
Claude 959770fa13 Gap 19: ODT lines of only pictures are as tall as the pictures
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
2026-11-02 14:51:51 +00:00
Claude adfcbcbf7c Gap 19: ODT pictures alone in a paragraph keep its size and their raise
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
2026-11-02 14:07:54 +00:00
Claude 8f65b867c0 Gap 19: ODT tables fit the margins and keep their spacing; header spacing kept
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
2026-11-02 13:38:36 +00:00
Claude 2f08e8bb01 Gap 19: ODT frames take their anchor and place from their style
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
2026-11-02 12:51:36 +00:00
Claude 3164c33c28 Gap 19: ODT letters keep one page: default size, page frames, box styles, empty lines
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
2026-11-02 12:05:17 +00:00
Claude 6a62c6142f Gap 19: docs: variables, emphasis marks, page backgrounds, tables, default size
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
2026-11-02 11:09:38 +00:00
Claude 48c60a51b1 Gap 19: ODT default font size; dates in the locale's order
- 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
2026-11-02 11:09:20 +00:00
Claude 7be387dabc Gap 19: ODT page background pictures
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
2026-11-02 10:36:41 +00:00
Claude 5cdf2608bf Gap 19: ODT tables fixed layout and starting a page; a page break before a table's first row moves it
- 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
2026-11-02 10:08:59 +00:00
Claude 078945e10c Emphasis marks: modelled, drawn, set, and kept in DOCX and ODT
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
2026-11-02 09:46:23 +00:00
Claude 56188b97f4 Gap 19: ODT variables as SET/REF fields; SET evaluated; character styles keep what they turn off
- 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
2026-11-02 08:37:37 +00:00
Claude bc5d9f9fa5 Gap 19: docs: ODT checked against LibreOffice's own templates
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
2026-11-02 08:02:33 +00:00
Claude e2b439ed17 Gap 19: ODT metafile pictures, pictures in text boxes, styles that turn things off
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
2026-11-02 08:02:11 +00:00
Claude 1dd4e64191 Gap 19: ODT page margins default as LibreOffice's; loose drawings join a paragraph
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
2026-11-02 07:03:12 +00:00
Claude ab02c85701 Gap 19: ODT fixes found by round-tripping LibreOffice's own templates
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
2026-11-02 06:36:36 +00:00
Claude 7848757954 Gap 19: ODT hidden form controls kept; .ott templates opened and saved
- 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
2026-11-02 05:49:16 +00:00
Claude ce516b04f6 Gap 19: docs: ODT fidelity re-estimated at ~90%
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
2026-11-01 23:46:56 +00:00
Claude 78bd6b2b72 Gap 19: a kept ODT form control made another kind saves as that kind
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
2026-11-01 23:46:56 +00:00
Claude 693ccff3b2 Gap 19: ODT form control sizes and buttons kept
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
2026-11-01 23:36:34 +00:00
Claude cb7de0227f Gap 19: ODT turned text in table cells and text boxes
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
2026-11-01 23:15:00 +00:00
Claude 2b826a288c Gap 19: ODT macros and unknown package parts kept
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
2026-11-01 23:14:59 +00:00
Claude 2036ad2e99 Gap 19: ODT table cell formulas as Word formula fields
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
2026-11-01 22:57:17 +00:00
Claude 7c9d301541 Gap 19: ODT custom shape equations worked out once, points kept finite
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
2026-11-01 22:57:17 +00:00
Claude 04ff4e3a54 Gap 19: ODT custom shapes drawn from their own geometry
A custom shape whose type WordCraft has no kind for (hexagon, octagon,
smiley, can, moon, lightning, block arc, cross, pentagon, flowchart
shapes…) opened as a rectangle. It is now drawn from its own geometry
(ODF 1.3 §10.6.2) as a freeform:
- the enhanced path (§19.145): moves, lines, curves, ellipses (T, U),
  arcs (A, B, W, V) and quarter ellipses (X, Y), arcs followed in
  short straight steps;
- its values worked out from the shape's equations (§10.6.3) and
  adjustment values: numbers, other equations, modifiers, the frame's
  keywords, + - * /, abs, sqrt, sin, cos, tan, atan, atan2, min, max
  and if.
Path length, equation count and nesting, and points are all bounded.

LibreOffice fills no part of a `draw:path` that has an open part. So a
freeform with both closed and open paths is now saved as a custom shape
whose path leaves the open ones unfilled (`F`).

LibreOffice's preset shapes render as LibreOffice draws them. 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
2026-11-01 22:37:45 +00:00
Claude 2877514d9e Gap 19: ODT bibliography marks as citations of sources
ODT citations (`text:bibliography-mark`) were read as their text, and
the sources they carry were lost. So an ODT bibliography had nothing
to update from.
- Reading: each mark is now a `CITATION` field of its source, which
  becomes one of the document's bibliography sources (the first mark
  of an identifier gives it). The identifier is the tag, the type the
  kind, plus author, title, year, publisher, address, journal, volume,
  pages, URL, editor, edition, number and month.
- Writing: a `CITATION` of a known source is written back as a mark
  carrying the source.

References › Update rebuilds an ODT bibliography from them. A
LibreOffice citation and bibliography round-trip pixel-identical and
validate against the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 22:13:47 +00:00