* chore: cut over image codecs and zip to craft-libs, fix main test regressions
* tests: store ODS mimetype and use standard ODF namespaces in mailings test
* Remove unused quick-xml workspace dependency
* fix: use standard ODF namespaces in file_dialogs test and preserve trailing line in markdown import_on_with_lines
* fix(geom): use png_chunks for PNG dimensions and p.fits(budget) in decode_for_drawing
- Wire craft-wordmodel, craft-docx, craft-rtf (doc), and craft-doc-binary into workspace
- Delete ~26,300 lines of app-owned DOCX parser/serializer in crates/docx
- Implement zero-copy media transfer and FitState custom XML round-trip via craft_wordmodel bridge
- Delete ~5,400 lines of app-owned RTF parser/serializer in crates/formats/src/rtf
- Delegate RTF import, export, parse, and esc to craft-rtf
- Add cfb_guard DIFAT validation and float sanitization on model bridge
- Net reduction: 31,697 deletions, 818 insertions (-30,879 LOC)
- Recognize a marked Fit One Page custom XML store across package and document
relationships, skipping passthrough only after loading successfully.
- Reserve the Fit One Page part path in EmbedWriter taken paths to prevent collisions.
- Track typography provenance per formatting location rather than sorted multisets.
- Validate stepPt parameter and reject non-positive or non-finite values.
- Report "already fits on one page" before an exhausted cumulative budget.
* docx: ignore w:lnNumType without w:countBy (#6209)
* DOCX: record the w:lnNumType rule in the parity docs (#6209)
A row for `w:lnNumType` (§17.6.8), its status line and a revision entry in
`docs/file-format-parity.md`, the progress-log bullet in `ROADMAP.md`, a comment in
the reader saying why a missing `w:countBy` leaves numbering off, and a save check in
the zero-interval test.
- Record original and reduced font sizes, line spacings and paragraph
spacings as multisets, so restoring neighbouring sizes clears the
exhausted budget, and paragraph spacing keeps the budget when line
spacing rounds back to its original.
- Resolve font and spacing budgets independently, letting the remaining
component be used instead of refusing the whole command.
- Persist FitState in a namespaced DOCX custom XML part and restore it on
read; add a DOCX round-trip regression.
In a multi-column section every line number was drawn left of the page
margin, so the second column's numbers sat on top of the first column's.
Each column now has its numbers left of its own edge, as in Word.
Word in other languages saves the built-in "line number" style under a
localised id (German: Zeilennummer); the numbers now find it by its
built-in name when there is no LineNumber id, so its formatting applies.
Fixes#6018.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
## What changed
The DOCX writer's content-control planner (`crates/docx/src/write/sdt.rs`) decides whether a control that
covers a whole paragraph is written block-level (`w:sdt` around `w:p`) or run-level (inside the paragraph).
It only treated the control as covering the whole paragraph when nothing but control-end markers followed
its end. Bookmark, comment and permission range markers after the end now count as "nothing" too, so the
control stays block-level and the markers stay at the end of its paragraph.
## Why
Word writes a body-level `_GoBack` bookmark (`<w:bookmarkStart w:name="_GoBack"/><w:bookmarkEnd/>` directly in
`w:body`) in nearly every file it saves. When the last body-level content control was followed by it, the
reader put the bookmark at the end of the control's paragraph, and the writer then turned the control (and
any controls nested in it) into run-level `w:sdt`s inside that paragraph. For a repeating section
(`w15:repeatingSection` / `w15:repeatingSectionItem`), which must wrap whole paragraphs or rows, Microsoft
365 Word then refuses to open the saved file ("the file appears to be corrupted"). Plain nested block
controls changed what they cover the same way.
Found by the real-file corpus: Word reported our saved copy of a form with a repeating section as corrupted.
## How it was tested
- New tests in `crates/docx/tests/it/nested_block_controls.rs` (synthetic packages): a repeating section around
one paragraph, plain nested block controls, nested block controls in a table cell, a run-level control that
must stay in its paragraph, and repeating sections / block controls followed by a body-level bookmark (with
and without data bindings) all round-trip with the controls at the same level and the text kept.
- `cargo xtask ci` passes.
Co-authored-by: tpawley2001 <268951182+tpawley2001@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #5843: resolve LibreOffice semicolon-separated font fallback lists to first installed family
* #5843: address review comments on font fallback list resolution
* #5843: verify fallback list text on cache hit and accept Times New Roman in tests
* #5843: bound the fallback lists read from a DOCX and reuse their classification
A document could name thousands of fallback families in `w:ascii`/`w:hAnsi`.
Nothing bounded that value, and the name-based rules (roman classification,
Thai family, named substitutes) then scanned every candidate for each shaped
piece, so opening the document took seconds.
`w:ascii` and `w:hAnsi` now keep the leading families within MAX_FONT_NAME
bytes, cut at a `;` so no name is split, as `w:cs`/`w:eastAsia` already bound
their names. The family a real list would pick is kept: a LibreOffice list
names a few families, well under the bound.
The Thai-family and named-substitute answers are worked out once per list and
kept in the fallback-list cache, keyed by the list text, so shaping no longer
rescans candidates for every piece. Resolution still reads every candidate.
A 500-paragraph document whose font names 20,000 candidates laid out in 10.8 s;
bounded on read it takes 17 ms.
Tests:
- a hostile fallback list keeps its leading families, cut on a `;`
- a real LibreOffice list is read whole
- a later Thai or named fallback still classifies, and lists are cached
Text nodes were decoded without line-end normalisation, so a file
saved with Windows line ends kept the CR of each CR LF: a line break
inside w:t read as two spaces. Text and CDATA now go through
quick-xml's xml_content, which reads CR LF and a lone CR as one line
feed; a CR written as stays a CR. Attribute values were already
normalised.
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
A file that turns an inline picture but writes no wp:effectExtent (or a zero
one), as other tools do, makes Word keep the picture's line to its frame and
let the turn overhang it, cut at the line's top and bottom: the frame as it
is in Word 2007 layout (compatibilityMode < 15), with width and height
exchanged from 45 to 135 and 225 to 315 degrees in Word 2013+ layout.
WordCraft always reserved the exact rotated bounds (Float::spin_pad), which
is what Word's own files encode through their effect extents, so the text
after such a picture sat up to 34 pt too low (docxide-pdf
standalone-picture-compat12-* / compat15-angle45/135).
The docx reader now marks such objects (Float::bare_turn, cleared when the
object is turned again); the inline room comes from Float::line_pad, which
display uses too to place the frame in it, and the line clips the picture's
overhang. The writer keeps writing no turn overhang for them, so a save
round-trips. Word-saved pictures are unchanged.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* WC6-07: preserve paragraph text direction
* Resolve gaps summary merge
* WC6-07: retain paragraph direction variants
* WC6-07: preserve strict paragraph direction
* Update lockfile for DOCX test dependency
* Close paragraph direction implementation
* Format paragraph direction imports
Compatibility-mode documents store text boxes as VML only. The reader
ignored v:textbox's text direction, so a header text box turned 270
degrees was drawn level. It now reads it as Word for Windows does:
mso-layout-flow-alt:bottom-to-top is vert270 whatever layout-flow says;
otherwise layout-flow:vertical or vertical-ideographic is vert; anything
else stays horizontal.
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>
A Strict document opened with its text and no shapes at all. Strict puts
shapes, groups and canvases (wsp, wgp, wpc) and their parts in its one
WordprocessingDrawing namespace, which the reader maps to wp:, while the
drawing reader looks for Word 2010's wps:, wpg: and wpc: names. Word also
writes a Strict text box's text in wne:txbxContent.
The XML reader now names those elements as Transitional documents do,
from the element and its parent (xml::element_prefix), and reads a text
box's content from wne:txbxContent and from the schema's own
txbxContent. Saving writes them as Transitional.
Claude-Session: https://claude.ai/code/session_01LvG6E1XTfHtZDbfYwfREhR
Co-authored-by: Filip Strajnar <filip.strajnar@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #5267: a linked picture shows and follows its link; DOCX keeps it on the drawing
A link put on a picture from its context menu was stored, but nothing showed
it: hovering the picture gave no tip, and Ctrl+click picked the picture for a
copy instead of following the link. Now the hover tip and Ctrl/Cmd+click work
on a linked picture, shape or text box, in line or floating, as on linked text.
DOCX: Word keeps a picture's hyperlink on the drawing (wp:docPr/a:hlinkClick,
ECMA-376 Part 1 20.4.2.5). It is now read (a w:hyperlink around the run wins)
and written beside the w:hyperlink, sharing one relationship. A link to a place
in the document stays the w:hyperlink w:anchor only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: ink and Office 2016 charts take their drawing's link; Ctrl+click over text on a Behind Text picture stays a text click
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: a chart or ink takes the link of the choice it was read from; Ctrl/⌘+click in the docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: the decoy choice in the chart-link fixture needs a namespace no reader knows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: inside a linked text box, hover and Ctrl/⌘+click take the box's link
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5267: the picture-link test sets the image's fx after merging main
Main gave InlineObject::Image picture effects (fx); the round-trip test this PR adds built
an image without them and no longer compiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* #4985: a numFmt none list level shows its literal text
Word labels a paragraph whose level has numFmt none and lvlText RESPONSE:
with RESPONSE:. Counters::next_label returned no label for any none level;
now only a none level of placeholders shows nothing. Its own %n stays
empty, and it still doesn't count. Numbering::shows_label agrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: a "none" list level counts, and only its own number is empty
Taken from MeHail-02's numbering-literal-none branch (46405e66), checked in Word
for Windows: a none level is an ordinary level whose own number reads as
nothing. It counts and restarts the levels below it, stays empty in legal
numbering, and an empty label still tabs the text to the indent unless it
already ends there (the Chapter 1 list's Heading 2 stays at the margin).
Co-Authored-By: MeHail <mihailsmetanin15@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF in full context keeps a none-level heading's parent
Review: a none level now has a label, so its headings reach the style index,
which leaves the none level out of the levels it keeps; \w and \r took the last
kept level for the heading's own and dropped the parent. Levels now carry their
index, and the levels above are picked by it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF number switches on a none-level heading
Review: in full context an empty label of its own added a trailing period (3.),
and the suppress switch gave a none level's label (Note), which holds no number,
back whole. An empty own part is left out, and a none level's label with no
number is suppressed to nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF \s and \t on none-level labels
Review: \s took a none level's label (Note) for a chapter number; the heading's
count stands in, as for an unnumbered heading. \t matched any ancestor's number
inside a word (the a of Chapter); only the numbers the label's template shows
count now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: STYLEREF takes a label's numbers by their spans
Review: \s took the last word of a label (Response of "3 Response") and fell
back on the heading count for a none level showing its parent's number (%1);
\t kept text attached to a number (Section3). A label's numbers are now the
spans of the values its template shows, with their delimiters (a letter number
only standing apart from other letters); \s takes the last of them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #4985: tests edit the numbering through Arc::make_mut after merging main
Main shares the numbering between undo steps (#2694), so `Document::numbering` is an
`Arc<Numbering>`; the tests this PR adds wrote to it directly and no longer built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: MeHail <mihailsmetanin15@gmail.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* fix(fields): quoted-argument backslash escapes and body PAGE in a cardinal-text section
A backslash inside a quoted field argument makes the next character literal, so TITLE "x \* Upper" shows x * Upper as Word does. A body PAGE field now uses the section's page number format (cardinal text shows words), as headers and footers already did.
* Fields: PAGE in a body table follows the section number format; parity docs
* fmt
* docs: clarify quoted field escape rule
* Fix page fields in fitted text boxes
* Move fitted cache test after layout helpers
---------
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
* WC6-06: preserve adjustRightInd in DOCX
* WC6-06: honor grid indent defaults and XML order
* Update lockfile for DOCX test dependency
* Format DOCX ordering assertion
* Keep main lockfile for paragraph property PR
* PR 5384: tighten grid indent docs and regression
* PR 5384: match paragraph check formatting
* docx: keep a paragraph's text alignment
w:textAlignment (ECMA-376 17.3.1.39) was not read, so a save dropped it. Word sizes a line by it when a
list's bullet is larger than the text: without it, those lines grow and every page after them moves.
ParaProps gains text_alignment, read and carried through styles, and written back after w:jc. Kept for
the file only; the layout does not use it yet.
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
* doc, docx: keep the compound border lines
The nine thin and thick compound lines (thinThickSmallGap to thinThickThinLargeGap, ECMA-376 17.18.2)
were read as a single line and written back as one, on paragraphs, tables, cells and pages. BorderStyle
gains them; they are written back by name and drawn as a single line of the border's width, as before.
RTF export and import carry them too (the importer read four of them as double).
* contributors: add entropybeater
---------
Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>