* ODT gap 19: text can turn off what its style turns on
A word not bold in a bold heading showed bold on screen, and came back bold in
LibreOffice after a save: an "off" on a run was dropped because the run's own
formatting said nothing, so the paragraph's or character style's showed through.
Keep it on the run for bold, italic, underline, strike-through, capitals and
small capitals, and write it back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: an Asian or complex normal says nothing of Latin text (gap 19)
With "off" now kept on the text, `style:font-weight-complex="normal"` alone took a
word's bold in a bold heading, and saved it as `fo:font-weight="normal"`. Those
attributes stay fallbacks only to turn it on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: strike-through turned off over a double one stays off (gap 19)
A style or span saying line-through none over a double strike still showed the double strike, on screen and after a save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: a missing full stop in the ODT revision row (gap 19)
Docs only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a span's character style wins over its paragraph turning off (gap 19)
A paragraph saying not bold made a span in a bold character style not bold too, on screen and after a save. The span's style now wins, for bold, italic, underline, strike-through and capitals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* ODT gap 19: custom styles with an outline level keep their name when edited
A paragraph in a custom style with style:default-outline-level was read as
WordCraft's Heading 1 or whichever level it named. Unedited, it was written as
read. Edited, it was saved as Heading_20_1, so LibreOffice showed WordCraft's
blue heading instead of the file's look, and WordCraft drew it that way too.
Read such a style as a custom style that carries the outline level. The
paragraph stays a heading at that level in WordCraft, is numbered with the
file's outline as LibreOffice numbers it, and is saved as
<text:h text:style-name="Head" text:outline-level="1">. The level comes from
the style, not the paragraph kind, because kinds stop at Heading 6.
The writer dropped a custom style's name on any heading, title, quotation or
code paragraph, so custom styles based on those lost their look when edited
too. It now keeps the name for every kind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a heading's own outline level gives its number (gap 19)
LibreOffice numbers a heading by its text:outline-level. WordCraft took the
number from the style's level, so a heading stating another showed the wrong
number, or one on a level the outline doesn't number. A heading in a list stays
the list's. Found by CodeRabbit on this PR, as was the Title-based style test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a heading in a list of its own keeps it, and an unnumbered level takes no number (gap 19)
Any list whose first level was linked to a style counted as the outline's, so a
heading deeper in one lost its list on save. Now only the number the outline
itself gives counts. A style based on a numbered heading, at a level the outline
doesn't number, took its parent's number. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a heading past level 6 keeps its list (gap 19)
The written outline has no number past Heading 6, so counting such a heading's
list as the outline's lost its number. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a style with a heading numbered by its own level stays on the outline (gap 19)
Writing it with no list style took the number from that heading in LibreOffice.
Written so, the numbers match the file's. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a heading past level six numbered through its style keeps its list (gap 19)
The written outline numbers only six levels, and the list was dropped for a heading numbered only through its style, so at levels 7 to 9 it lost its number once its style was rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* ODT gap 19: pictures keep the size their file gives
Opening a file fitted a picture wider than the text to the text, and shrank any
picture wider than 6.5 in to that, even with its size stated. So a save with no
edit to it took its paragraph off being written as read, and LibreOffice laid
it out differently. Pictures in OpenDocument files keep the size they state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a picture keeps a size stated on one side, in proportion (gap 19)
Only a frame stating both sides kept them. On one side alone a picture past a
column's width was still fitted to it, and past 1,584 pt each side was cut
separately, which distorted it. A size past any page can no longer overflow the
sizing either. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: an inline picture with no stated size is fitted to the text (gap 19)
Skipping the fit for OpenDocument files left one wider than the text on a small
page, which `main` fits. A size the file states still stands, and a test now fails
without that. The fit happens when it is read, so its paragraph stays as read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: an object's picture is fitted too, and one side follows the picture's own shape (gap 19)
An inline object with no stated size kept its picture's width on a small page, which main fitted. A picture stated on one side took the other from the size already capped to the page: 10 by 1000 pixels at 500 pt wide came out 468 by 1584 pt, not 15.84 by 1584.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a frame stating a size of nothing is fitted like one stating none (gap 19)
svg:width="0pt" counted as a stated size for fitting, though the picture then came at its own size, so it could run past the text on a small page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a picture in line that is raised, turned or mirrored is fitted too (gap 19)
Such a picture has a float record that is still in line, which the reader's fit skipped, so with no size stated it stayed 468 pt wide on a small page. main fitted it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Order the parity header's change notes newest first
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* ODT gap 19: frames keep the page they are anchored to
A frame or shape anchored to a page was written without its
text:anchor-page-number, so one on page 3 came back on page 1 in LibreOffice
after a save. Keep the number and write it back. WordCraft's own layout still
places such a frame on its anchor paragraph's page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a page number only counts on a page anchor, and groups keep theirs (gap 19)
A paragraph anchor with a text:anchor-page-number was saved as a page anchor,
which LibreOffice ignores the number on and so moved the drawing. A group
anchored to a page lost its page. Found by CodeRabbit on this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a frame moved in WordCraft loses its page number (gap 19)
Nothing cleared it, so the save put the frame back on its old page. One placed by
its paragraph is no page anchor any more, and one dropped on a page takes that page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT tests: put the moved frame's test with the other moves (gap 19)
At the end of the file it collided with other ODT PRs' tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a frame moved onto its paragraph loses its page number (gap 19)
The number was only hidden while the frame was placed by its paragraph, so placing it by the page again (arrange.position) brought back the old page. The merge of main also lost the test attribute on a test from main, put back here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: parity row says when a moved frame keeps its page number
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* ODT gap 19: a document keeps its language when styles are rewritten
The default language was only ever saved by copying the file's own styles
back. Adding numbering or creating a style writes them from the model, which
never stated it, so LibreOffice showed the document in its own default
language. Import ignored it too, so such a document was proofed as English and
the styles WordCraft adds said en-US.
- Read the language from Standard, else the default paragraph style
- Write it in the model-written default style
- A file stating none has none, rather than en-US from new documents
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a language keeps its script, and a style with no parent the default style's language (gap 19)
sr-Latn-RS was written as fo:country="Latn" with RS dropped, and fo:script
wasn't read, in all three of the language attributes. A style with no parent
took Standard's language when it differed from the default style's. Found by
CodeRabbit on this PR, as were the added assertions and doc comments.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: the language written is Normal's, as WordCraft has it (gap 19)
The default style took its language from the defaults, though WordCraft's document language is Normal's: a Word file with a French Normal over en-US defaults saved as English.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* ODT gap 19: table and row backgrounds show behind their cells
fo:background-color on a table or row wasn't read, so it was missing on screen
and gone in LibreOffice after a save. WordCraft's tables have their backgrounds
on their cells, so it goes on the cells with none of their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a column's default cell style shows, and transparent clears a background (gap 19)
A cell with no style took the row's or table's colour over its column's default
cell style, and `transparent` on a table or row fell through to its parent style's.
From CodeRabbit on this PR, checked in LibreOffice 26.8.2.2: a row's default cell
style isn't applied there, so it isn't here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a column's default background is read once, not its style's name (gap 19)
The name was cloned for every repeat of a column, which a hostile file could make
use to run out of memory, and allocation failure can't be caught.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ODT: a transparent cell style stops its parent's colour (gap 19)
A cell style saying transparent over a coloured parent took the parent's colour and hid the row behind it. LibreOffice 26.8 shows the row, from a column's default cell style or the cell's own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Gap 19: docs status lines are main's, with this change in front
Merging main had kept this branch's older status lines in ROADMAP.md and
docs/file-format-parity.md, which set main's 2026-11-08 date back and dropped
main's newer entries and the parity doc's CLI path note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Moves craft-update, craft-pdf and the krilla patch from storytold/craft-libs
c5a1cbfb to dotCooCoo/craft-libs c7c84230, which is craft-libs main at
0a9d1f48 with storytold/craft-libs#687 and #696 merged in.
- The result window opens whenever the updater's status is not idle: at
the first start after an update it says the version that was installed
or why the update failed, and Help > Check for Updates shows Installing,
Ready and the launcher's choice. A check asked for from the ribbon now
shows the window on the next frame.
- Restart to Update closes the window and asks the app to quit. An update
that asks the app to close quits every window, after asking about each
document with unsaved changes.
- on_exit and the macOS terminate path start the waiting update's helper.
- The update prompt and the result window draw their words through the
interface catalog.
* fix(render): skip drawing paths far past every canvas before flattening
* fix(render): refuse paths with non-finite points too
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* fix(docx): collect an embedded object's relationship ids in a set on save
* style: rustfmt
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* Charts: an automatic series colour follows the series' c:idx (#5963)
A series without its own colour took the accent for its place in the
file. Word picks it by the series' c:idx (c:order doesn't matter), so a
chart whose series aren't numbered 0, 1, 2... in file order, such as a
lone series with idx 2 after a deletion, got different colours.
c:idx is read strictly as the unsignedInt the schema says; a missing or
invalid value falls back to the place in the file.
* Charts: the renumbered test helper fails if ser's layout changes (#5963)
---------
Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
## What changed
Word stores paragraph indents (`w:ind` left/start, right/end, hanging, firstLine) in 16 signed bits and paragraph
spacing (`w:spacing` before/after) in 16 unsigned bits. A file that writes a larger plain integer is read by Word
modulo 2^16: `w:left="66256"` is a 720-twip (0.5") indent in Word. We read the full value, so such a paragraph was
indented by about 46 inches and its text was pushed off the page.
- `crates/docx/src/units.rs`: new `tw_i16` / `tw_u16` readers. A plain integer keeps its low 16 bits (two's
complement for indents, so 66256 -> 720 and 65535 -> -1); values with a unit or a fraction read as before.
- `crates/docx/src/read/props.rs` and `read/styles.rs` use them for `w:ind` and `w:spacing` before/after, in direct
and style formatting.
- In-range values read exactly as before.
## Testing
- `crates/docx/tests/it/twips_wrap.rs`: synthetic documents with out-of-range indents and spacing in paragraph and
style properties check the wrapped values; in-range values and values with units are unchanged.
- Unit tests for the two readers; `cargo xtask ci` passes.
Found by a real-file corpus comparison against Word's own PDFs: a file whose indents exceed 16 bits shows its text on
the page in Word and off the page here.
Co-authored-by: tpawley2001 <268951182+tpawley2001@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Word treats the end-of-cell mark as a paragraph in the default (Normal)
style, so a cell's last paragraph with contextual spacing in that style
(or in an undefined style, formatted as Normal) loses its space after, as
beside any same-style neighbour; one in another style keeps it.
`layout_box` kept the space after of every last cell paragraph except
automatic spacing, so the row grew by it and the text below the table sat
that much lower (docxide-pdf cell-end-text-context, cell-end-empty-context:
8 pt).
A middle or bottom cell of a row split across pages lines up with the
other cells' kept text, which `align_in_part` counted with its full space
after; it now stops at the cell's content end, so a dropped space after
(contextual or automatic) no longer pushes the aligned cell down.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>