* .doc import: inline pictures, floating pictures and basic shapes (#551)
Word 97-2003 files kept their text but dropped every picture and drawing.
Pictures and drawings are now read from the OfficeArt records ([MS-DOC],
[MS-ODRAW]):
- Inline pictures: the PICF's OfficeArtInlineSpContainer (MM_SHAPE /
MM_SHAPEFILE) and its FBSE/BLIP, with crop and alt text from the shape
properties; the older raw-image path stays as a fallback.
- Floating pictures and shapes: PlcSpaMom/PlcSpaHdr anchors joined with
the shapes of the OfficeArtContent drawings (fcDggInfo), placed by the
SPA bounds, bx/by or posrelh/posrelv/posh/posv, wrap type and
fBelowText; rotation and flips; pictures from the BLIP store.
- Shapes: rectangle, rounded rectangle, ellipse, diamond, triangles, star,
block arrows, heart, lines/connectors, with fill/line colour and width;
groups flattened into one group space; text boxes linked to their
PlcftxbxTxt story parts by spid.
- BLIPs: PNG, JPEG, TIFF as-is, DIB converted to PNG, EMF/WMF inflated
(the metafile renderer draws them), PICT kept as a sized placeholder.
- Hostile input: record lengths clamped to their parent, nesting depth
and record counts capped, decompression size-limited, picture bytes
capped per document as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #558: miniz_oxide as a workspace dependency
Restores the change @echelon pushed in 04b59287, which a rebase dropped: miniz_oxide is declared once in the root [workspace.dependencies] and docbin uses it with workspace = true.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Indents in characters (w:firstLineChars, w:hangingChars, w:leftChars,
w:rightChars; sprmPDxc* in .doc) are kept in the model, read and written,
and laid out in the paragraph mark's font size, winning over the points
as in Word. Indent commands cancel an inherited character indent.
- .doc: sprmPDxaLeft1 has a two-byte operand (was read as four, so every
first-line indent was lost); sprmPDxaLeft1_80 is read; LSPD line spacing
is decoded as signed dyaLine + fMultLinespace (exact 20pt read as 799pt,
a page per empty paragraph).
- A floating table placed on the page or margin that would cover the text
already on the page and doesn't fit below it moves to the next page.
- A line holding only a page break stays at the bottom of a full page
instead of leaving a blank page.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Numbering: Chinese, Japanese and Korean number formats (一、二、三 …) (#534)
A list with w:numFmt="chineseCounting" and lvlText "%1、" showed "2、", and the
digit was drawn with glyph ids of the CJK font that supplied the comma, so it
came out as an unrelated letter: the list label was laid out in font runs but
drawn with the style of its last run only.
- NumFormat knows every ST_NumberFormat value (ECMA-376 §17.18.59), so a file
saves back the format it was opened with; unknown values are decimal.
- New doc::cjk_numbers: Chinese/Taiwanese counting (with 十 below 100, digit by
digit above) and counting-thousand (百/千/万/亿, 零 for gaps), simplified and
traditional legal numerals, Japanese counting/legal/digital ten-thousand,
Korean counting/digital/legal, ideograph digits, heavenly stems, earthly
branches and the sixty-year cycle, aiueo/iroha (half and full width),
ganada/chosung, enclosed numbers (①, ⒈, ⑴, ㊀) and full-width digits. Values
past a sequence's end fall back to decimal.
- Legal numbering (w:isLgl) shows every level, the current one too, in decimal.
- List labels keep their font runs and draw each with its own face.
- A list level without w:ind no longer gets a 36pt indent on read.
- .doc number format codes 0x00-0x27 map to the formats (0x16 decimalZero).
- PAGE/NUMPAGES fields take \* ChineseNum1/2/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt after merging main
* Layout test: end the list range inside the third paragraph (#477 skips a paragraph the range ends at the start of)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content controls: keep, show and edit w:sdt (#381)
Content controls are now part of the model instead of being unwrapped on open:
ControlStart/ControlEnd markers around their content (inline or around whole
paragraphs), with table, row and cell wraps for controls around tables,
repeating-section rows and cells. All sdtPr properties round-trip (title, tag,
id, lock, placeholder, temporary, rich/plain text, w14 check box, combo box,
drop-down list, date, picture, building block gallery, w15 repeating
sections); unknown children are kept verbatim. The writer plans per block list
which controls are written inline and which around blocks so the XML always
nests, and drops markers that lost their partner.
Editing: deletions keep a marker whose partner is outside the range, typing
replaces a shown placeholder, emptying a control brings it back, sdtLocked and
contentLocked refuse with a status message. Developer tab with insert
commands, Design Mode, Properties dialog; frames and title tabs on the page,
check boxes toggle on click, list and calendar pickers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #381: refresh catalog count and parity checklist after rebase
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.
New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.
The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Follow [MS-DOC] in the Word 97-2003 reader:
- Prm: bit 0 is fComplex; Prm0 isprm in bits 1-7, Prm1 Prc index in bits 1-15 (#458)
- sprmSBkc operands 0-4 map to continuous/new column/new page/even/odd (#459)
- sprmSBOrientation 1 = portrait, 2 = landscape (#460)
- negative sprmPIlfo (0xF802-0xFFFF) references the negated LFO index (#461)
- StdfBase: istdBase shares the word with stk and is zero-based; the
whole base chain is applied, bounded and cycle-safe (#462)
- SttbfFfn entries advance by cchData, covering the optional xszAlt (#463)
One regression test per issue, built in code from the spec.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Formatting changes are tracked like insertions and deletions:
- Model: runs and paragraph marks carry the formatting before a tracked
change (CharProps::fmt_change), paragraphs their old properties
(ParaProps::fmt_change) and a list numbering change; tables, rows,
cells and sections their old properties.
- DOCX: w:rPrChange (runs and paragraph marks), w:pPrChange,
w:numberingChange, w:tblPrChange, w:trPrChange, w:tcPrChange and
w:sectPrChange are read and written back, each last in its parent
(ECMA-376 17.13.5). Nested changes are ignored; ids are renumbered.
- Recording: with Track Changes and Track Formatting on, Session::run
compares the paragraphs a command changed with the document before it,
so every formatting command records without per-command code. An
author's later changes keep the formatting before the first one;
formatting set back to the original stops being a change.
- Accept/Reject (at the caret, selection, all) keep or restore the
formatting; Next/Previous and the Reviewing Pane list format changes
with a "Formatted: Bold, Size: 14" description.
- Markup: change bars beside formatted lines and "Formatted: ..." balloons
in the markup area, per Track Changes Options.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)
Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.
The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.
The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)
Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.
The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.
The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Define New Multilevel List and Track Changes Options dialogs (#328)
Home > Paragraph > Multilevel List > Define New Multilevel List edits all nine
levels with a live preview: number text and style, start, restart after a
level, linked paragraph style, alignment, aligned-at and text indent,
what follows the number with an optional tab stop, and the number's font. OK
runs the new list.define command (list.get reads a definition back). List
levels gain a restart-after level and a tab stop, which the counters and
layout honour and .docx reads and writes (lvlRestart, num tab); number
alignment now places the label.
Review > Tracking's launcher opens Track Changes Options: show comments, ink,
insertions and deletions, formatting; balloon mode; insertion and deletion
marks and colours; changed-line bars and their colour; track formatting. They
go through review.trackingOptions, are saved per user with the interface
preferences, and the page drawing honours the marks, colours, hidden
deletions, change bars and inline mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Parity checklist regenerated after merging main into #342
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* D0+D1: open Word 97-2003 .doc — container, FIB, piece table text (#28)
New wordcraft-docbin crate (L2) reads the OLE2 container via the cfb
crate (MIT) and parses the FIB and piece table from the [MS-DOC]
specification: text pieces decode as UTF-16LE or Windows-1252 (table
moved to wordcraft-doc::encoding), subdocument CP ranges, and Word's
special characters map to the model's (tabs, breaks, hyphens). .doc/.dot
registered in OPEN_EXTS and the I/O dispatch. Encrypted and Word 6/95
files give clear errors; every length is checked and proptest
truncation/flips never panic.
* D2: .doc formatting — stylesheet, fonts, CHPX/PAPX FKPs, sprms (#28)
Sprm decoder with operand-size rules (incl. sprmTDefTable/sprmPChgTabs
exceptions and the Prm0 isprm table), bin tables + ChpxFkp/PapxFkp page
lookup, STSH styles (StdfBase layout validated against real Word files:
bchUpe == cbStd, sti 0 = Normal) and the SttbfFfn font table in both
extended and self-delimiting flavours. The CP walk now splits runs on
formatting changes and resolves paragraph styles: bold/italic/strike/
underline/caps/hidden, colours (Ico palette + COLORREF), half-point
sizes, super/subscript, fonts, highlights; alignment, indents, spacing,
keep options, outline levels. Unknown sprms are skipped leniently;
an env-gated corpus test opens real files (styles + text asserted).
* D3: .doc structure — sections, headers/footers, tables, lists (#28)
- Sections: PlcfSed/Sepx with 16-bit measurement sprms (sprmSXaPage & co),
explicit sprmSBOrientation with a width/height fallback; even/odd/first
header and footer stories from Plcfhdd become document parts.
- Tables: sprmTDefTable grids (operand length fixed to cb+1 per the spec),
horizontal merges (sprmTMerge), vertical merges (sprmTVertMerge), cell
shading (sprmTSetShd / sprmTDefTableShd80), cell widths, valign, row
height/header/cant-split, and table borders (sprmTTableBorders80 Brc80s).
- Lists: PlfLst (LSTF + appended LVLs) and PlfLfo (LFO + LFOLVL overrides)
resolved into the numbering model; sprmPIlvl/sprmPIlfo mark paragraphs.
- Combined fixture (intro + table + list + 2 sections + header) round-trips
through Document JSON; hostile proptests now mutate the structured fixture.
* D4: .doc notes, fields, bookmarks and inline pictures (#28)
- Footnotes/endnotes: PlcffndRef/PlcfEndRef reference CPs paired with the
PlcffndTxt/PlcfEndTxt story ranges; each story walks into a Footnote or
Endnote part and the 0x02 anchor becomes a NoteRef object.
- Fields: 0x13/0x14/0x15 state machine in the walker — instruction text is
hidden, the cached result kept; HYPERLINK fields keep their result as
linked runs, everything else becomes one Field object.
- Bookmarks: SttbfBkmk names + Plcfbkf/Plcfbkl CPs become BookmarkStart/
BookmarkEnd objects at their anchors.
- Pictures: sprmCPicLocation points into the Data stream; the PICF header
gives the display size (dxaGoal/dyaGoal scaled by mx/my) and the payload
is format-sniffed (PNG/JPEG/GIF/BMP; metafiles are dropped with a warn).
- Hostile proptests now truncate/flip a fixture with all of the above.
* D5: .doc import hardening, caps and roadmap (#28)
- Cap inline pictures at 5 000 per document.
- Engine test: .doc/.dot bytes dispatch through the Word 97-2003 reader
(garbage must error, never panic or open empty).
- ROADMAP: Word 97-2003 import row (60% — text, formatting, styles,
sections, headers/footers, tables, lists, notes, fields, bookmarks,
raster pictures).
Real-corpus validation stays opt-in: WORDCRAFT_DOCBIN_FIXTURES=<dir>
cargo test -p wordcraft-docbin corpus_files_open.
* Corpus fixes: compressed-piece FC space, PAPX query point, none-level labels (#28)
Validated against two real files (kept locally under plan/word/fixtures,
gitignored): a 1 MB Lorem-ipsum sample and a 2003 styles/lists/table
sample.
- FKP bin tables and pages address the WordDocument stream in true byte
offsets; a compressed piece stores its base fc doubled (byte = fc/2),
so fc_of_cp now unwinds the doubling. Before, every PAPX/CHPX lookup in
compressed (cp1252) files landed on the wrong entry — tables collapsed
to one row, lists and heading styles were lost.
- The paragraph-properties query now uses the paragraph mark's own FC
(inside the paragraph) instead of the FC after it, which picked up the
next paragraph's FKP entry.
- Fixture papx entries re-keyed to paragraph-start CPs, the real rgfc
convention.
- Counters::next_label returns nothing for NumFormat::None levels (Word's
numFmt none shows no label; also fixes a latent docx-reader case).
- Symbol/Wingdings bullet chars map to Unicode glyphs (• ▪ ○ ●).
With this, both corpus files render headings, styles, nested and numbered
lists, the tables and the PAGE field correctly; metafile-wrapped images
and OLE objects stay dropped by policy (logged).
* docbin: bound the text walk by real text; dedupe and cap pictures (#28)
Hostile CP numbers no longer drive the work:
- header/footnote/endnote story ranges from the Table stream are clamped to their
subdocument (ccpHdd/ccpFtn/ccpEdn) with checked base additions, and every walk stops at
the last piece's CP; a story spanning [0, 4e9) used to push billions of U+FFFD;
- characters are found by binary search over the pieces (PieceTable::char_at), and an
unreadable stretch (gap, truncated or out-of-stream piece) is skipped in one step rather
than one U+FFFD per CP; the old per-character scan was O(chars x pieces);
- the piece CPs are made non-decreasing at parse, so inverted pieces are empty and binary
search stays sound; surrogate pairs now decode;
- note references are looked up by binary search; the style's CharProps is borrowed, not
cloned, per character;
- pictures are interned by fc_pic (5000 anchors on one picture share one media entry and
one copy) and the bytes copied per document are capped at 256 MB.
Each fix has a crafted-bytes regression test; against the previous code the story,
piece-claim, last-piece and many-pieces tests hang (killed at 60 s) and the shared
picture test copies the picture once per anchor (80 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docbin: checked offset arithmetic in lists, sections and table rows (#28)
- list.rs: PlfLst/PlfLfo/LVL offsets (at + 2 + cLst*28, at + 4 + lfoMac*16, off += len,
...) saturate, which matters on 32-bit targets (wasm32) where fc + n can overflow usize;
the LFO override path no longer indexes abstracts directly.
- sections.rs: the Sepx range (fc_sepx + 2 + cb) is checked; Plcfhdd is read only as far
as the stories a capped section count can use (it used to reserve lcb/4 entries).
- table.rs: sprmTDyaRowHeight = i16::MIN negated in i16 (overflow panic in debug builds);
now -(v as f32).
Regression tests: the row height test failed before (overflow panic with debug assertions, a 0 pt height without); the list test pins the saturating
offsets (it only overflowed on 32-bit targets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the merged OPEN_EXTS list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>