Conflicts kept both sides. Where the two branches met:
- Para.mark: both added it. It stays CharProps (ODT needs it); HTML's
`<p> </p>` converts its Fmt, and to_doc reads it in one place.
- Arrowheads: the PR's line ends (top left to bottom right) with main's
arrowhead styles.
- inline_rect: the PR's depth below the baseline with main's rule room.
- Spacing and Cell literals take main's new fields.
- The zip entry guard (#3660) also covers the encrypted ODT paths, which
parsed a package before it; regression test in tests_more/encryption.rs.
- chunks_exact to as_chunks in odt/frames.rs and odt/path.rs (clippy).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment, tracked change or index mark reaching from one paragraph
into another, or a text box continuing in another's, ties paragraphs
into a group: written as read while every paragraph of it is as read,
else all from the model, which names what ties them afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Opening draws charts from their data, which changed their paragraphs'
fingerprints, so their markup never matched. Drawing goes through
Document::fill_objects, which moves the kept markup with them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section in a table's cell is kept around the cell's paragraphs and
closed in the cell. One starting or ending at a paragraph with twins
(an empty one) is kept by that paragraph's own identity; the writer
opens each kept section once, so a copy can't open it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Every element of the ODF 1.3 schema checked against both corpora and a
document of our own for each one no file held (docs/odt-element-audit.md).
Found and fixed: index and reference marks over text, plugins and
floating frames weren't kept as read, and a form's data source was
taken for a control. Left: password and grid form controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
Found checking 220 ODT files from other applications (local only):
- What a section hidden with text:display="none" holds is read as
hidden text, so it stays hidden written without the section.
- A top-level list whose items are all as read is written as read
whole (label spacing, picture bullets, list headers); a list going on
with it names it; a section may begin or end with a list.
- A damaged package's media type isn't printed in its error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A kept part without a media type (LibreOffice's empty accelerator
configuration, a file the manifest didn't list) was written without an
entry, and LibreOffice refused the saved file. ODF 1.3 Part 2 §4.3
lists every file; its media type stays empty as read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Paragraphs and tables holding bookmarks are kept; the writer takes
the names kept markup begins and ends, so either end may be as read.
- Footnotes and endnotes: each note's text fingerprinted when read;
kept while it and the note numbering are as read.
- Formulas, charts and other objects in kept markup, with their files
and preview pictures (only while that markup is written).
- Page-anchored frames LibreOffice puts in the text before a paragraph
are kept before its markup.
- Styles of different families sharing a name are kept apart.
- The text keeps text:use-soft-page-breaks while kept markup holds them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
The paragraphs a drawing's text holds count as its story's, so the
paragraph showing it can be kept. Each text box's and shape's story is
fingerprinted when read (with the boxes and notes it holds); one edited
sends its paragraph to the model. Linked boxes and a comment inside a
box's text stay with the model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph whose comments (range, replies, end) or tracked changes
(start and end) are all its own is kept as read. Each top-level comment
is fingerprinted with its replies when read; one edited, resolved,
answered or deleted sends its paragraph to the model, as a reply written
elsewhere in the file does. The change regions kept markup marks are
written as read (those their deleted content marks first), beside
WordCraft's own, which take the writer's prefix; one without a date is
written from the model. An older file's region gets its ODF 1.3 xml:id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A paragraph or table holding pictures and shapes without text is kept
as read while unchanged, with the pictures it shows (only those linked
from markup actually written, so a deleted picture stays deleted) and
its frames' styles. Text boxes, shapes with text, objects and controls
stay with the model. The file's Graphics and Frame styles are written as
read; WordCraft's own pictures don't take the file's picture names. The
flat writer inlines a picture whatever form its element has.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
A section at a text box's top level keeps its wrapper, like the body's;
the box's story closes whatever it opened, so an edit inside the box
can't leave a section open past it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
- Kept definitions are keyed by name space (a list style may share a
paragraph style's name, §16.2), so unused list styles survive a save.
- Section wrappers kept as read go around tables as well as paragraphs,
nested ones too, balanced however the blocks are edited.
- An unchanged chart keeps its replacement picture.
- Note citations use the document's number format and first number.
- draw:z-index is read into the float's stacking order and written back.
- The no-edit gate counts elements, catching a section of three lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
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 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
Word's TOC over entries in columns keeps its TOC Heading title in a one-column
section above a blank paragraph that ends it; Update Table moves the field into
that blank paragraph. Replace (references.toc with settings) put the new title
under the break, inside the two-column section, and Remove kept the blank
paragraph and its break.
Observed black-box in Word for Mac 16.114.3 on a synthetic document: Insert
Table of Contents keeps the title, the blank paragraph and the break and only
changes the field; Remove Table of Contents takes the title, the blank
paragraph and its break with the entries.
Replace now keeps the title above the break (renamed when a title is given)
and rebuilds the field and entries in place; Remove takes the title and the
blank section-ending paragraph with the table. A paragraph of another style
above the break stays.
Co-authored-by: Harrison Smith <harrison@hsmith.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
When a region is chosen in File › Options, start-up replaces the first blank document with one
on that region's paper through `session.set_document`. That changes the session's active
document, but the window still showed the old one, so on the first frame `sync_document` took
the change for a switch of document and cleared the dialog. A password-protected file named on
the command line (a double-click in the file manager) had just asked for its password with
`file.open`, so the Password box was dropped before it was drawn and a blank Document1 stayed.
`WordApp::replace_startup_document` swaps the document and lets the window follow at once; the
start-up code uses it. Before the file is opened, so nothing is open to lose.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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
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
* DOCX: VML horizontal rules take their line
A web page's <hr> pasted into Word is a VML rule: `v:rect` with
`o:hr="t"`, often `width:0`. WordCraft took the width as written and
drew a 1 pt dot where Word draws a line across the page.
Measured on Word with a synthetic document: a rule with no width and
no `o:hrpct` spans the line inside the paragraph's indents; `o:hrpct`
is a share of it in tenths of a percent (500 = half); `o:hrpct="0"`
keeps the shape's own width; `o:hralign` places a shorter one (left,
centre, right); `fillcolor` and `stroked` apply as to any VML shape.
WordCraft now matches each case within 0.1 pt.
- wordcraft-doc: ShapeExtra::hr (HorizontalRule: share, alignment,
Word's standard look).
- wordcraft-docx: `o:hr`, `o:hrpct`, `o:hralign`, `o:hrstd` read; a rule
is written back as the same VML rule (DrawingML has none), and Word
opens the re-saved file with the same lines.
- wordcraft-layout: a rule takes its whole line and is drawn at its
share; measured for a table column it asks for no width, so it
follows its column. (Word draws a rule in a cell at the page's text
width, centred on the cell and past its edges; it stays in the cell
here.)
docs/file-format-parity.md is updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: only an inline, shown rule is one
Review: the rule arm of the writer ignored the shape's placement, so a
rule imported with position:absolute or visibility:hidden came back
inline and visible. Only a rule in the text and shown is read as a rule
now (a pasted <hr> is one); a positioned or hidden one stays a plain
shape, which keeps both when saved. The writer checks the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: a see-through fill is kept; column measuring asks for the right width
Review: `write_hr` wrote only the fill's colour, so a rule with `v:fill
opacity` came back opaque after a save; the opacity now goes back as `v:fill`.
Review: measured for a table column, every rule asked for no width, so one of
its own width (`o:hrpct="0"`) could end up clipped by a narrow column; it now
asks for its width. AutoFit to contents measures on a line of its own width
that the rule took for a real line, so a rule sharing its line made the
column as wide as it could go; any measuring width now counts.
`Placed::Shape` boxes its extras: with the rule, `Placed` went past clippy's
large_enum_variant limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Horizontal rules: placed in what the line has left; rotation and flips kept on save
Review: a whole-line rule ignores a first-line indent or a line narrowed by a
float. Measured in Word, it does too: the width stays the share of the
paragraph's width and the rule runs past the margin, or under a float on its
right. What did differ was the placement: Word centres and right-aligns a rule
in what the line has left, with a first-line indent moving the end of that room
along, and a right-aligned rule beside a float was drawn off the page here. The
display now places it that way (`ParaLayout::rule_room`).
Review: a save dropped a rule's rotation and flips; they now go back in its
style, as Word writes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: VML horizontal-rule revision row at the top of the history
---------
Co-authored-by: Daniele Mori <Dinasty@hotmail.it>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Shapes: a solid fill keeps its transparency
A shape's fill colour can carry a:alpha, its opacity. WordCraft dropped
it, so a translucent white wash laid over a picture was drawn opaque and
hid the picture. The transparency is now read into the shape, drawn on
screen and in the PDF, and written back.
* Fix#1600 freeform and soft-edge fill opacity
* #1600: wrap canonical caret sampler signature
* #1600: format canonical integration
* #1600: assert canonical DOCX alpha round-trip
* #1600: document canonical shape opacity API
* Adapt alpha fixture to rectangle render bounds
* docs: one file-format-parity row for soft-edge fill opacity, status line
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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