Commit Graph
7 Commits
Author SHA1 Message Date
f7a84fbfb8 Print Markup: comments in PDF, print, images and text copies (#589) (#667)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:38:29 -04:00
baeb8d9c6c Open and save Word XML Document (Flat OPC); open Word 2003 XML (#548) (#560)
- docx: Flat OPC (`pkg:package`) read by unpacking the parts into an
  in-memory package for the DOCX reader; written from the DOCX writer's
  parts (XML inline, binary parts in base64). Part count, part size,
  total size and base64 length are capped.
- docx: Word 2003 XML (`w:wordDocument`) imported by rewriting it as the
  corresponding package (hyphenated names, w:hlink, w:listPr, w:lists,
  inline headers/footers, notes, aml annotations, w:binData pictures,
  o:DocumentProperties) and reading that with the DOCX reader.
- `.xml` told apart by root element; other XML is refused with
  "not a Word XML document". File > Export gets "Word XML Document (*.xml)"
  (all 9 catalogs); .xml saves keep the document like .docx.
- Tests: Flat OPC round trip equals DOCX round trip, hand-written Flat OPC
  package, Word 2003 import, hostile inputs, engine io.

Fixes #548

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:59:00 -04:00
663904a2e2 Import Word 97-2003 .doc files (#28) (#145)
* 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>
2026-10-10 08:36:38 -04:00
AiBizzAppsandClaude Opus 5.5 a45c8a41de DOCX: keep a .docm's macros and signatures on save; right content types for .docm/.dotx/.dotm (#172)
* DOCX: keep a .docm's macros on save; write .docm/.dotx/.dotm content types

Saving a .docm deleted its VBA project and wrote the plain-docx main
content type under the .docm name, which Word rejects; .dotx was written
as a document, and .dotm could be opened but not saved. io_ext sent every
Word extension to wordcraft_docx::write, which always emitted a .docx
package, and the reader never kept the VBA parts.

- wordcraft_docx::Flavor (docx/docm/dotx/dotm) picks the main part's
  content type; write_as(doc, flavor) writes it, write() stays .docx.
- The reader keeps word/vbaProject.bin (and the vbaData.xml it relates
  to) as opaque bytes in Document::passthrough, the mechanism custom.xml
  already uses. Macro content is never parsed or run. The writer emits
  them, with their relationships and content types, only for .docm/.dotm;
  a .docx/.dotx leaves them out cleanly (as Word does) and the open
  document still holds them for a later Save As .docm.
- Empty projects, missing or malformed VBA data, duplicate or odd
  relationships never panic and never stop the document from opening;
  part sizes stay under the existing package limits.
- .dotm joins the open/save lists, and saving to .docm/.dotx/.dotm binds
  the session to that file like .docx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DOCX: carry vbaData.xml as opaque bytes, never parse it

The first cut gated word/vbaData.xml through xml::parse on read and on
write. That parser rejects UTF-16 (a valid XML encoding), so a
UTF-16 vbaData.xml was silently dropped and the saved .docm kept
vbaProject.bin without its VBA data; and it is lenient (no end-name
check, auto-closes at EOF), so malformed data passed the gate anyway.
WordCraft is a passthrough for macro parts: it should emit exactly what
the user's file had. Now vbaProject.bin and vbaData.xml are both carried
byte for byte (bounded by the reader's existing size limits); only the
relationship part is read, to find the VBA data. A relationship to a
missing VBA data part keeps the project and writes no dangling link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DOCX: read UTF-16 XML parts instead of dropping them

xml::parse rejected any part with a UTF-16 BOM, and Package::rels
swallowed that error as "no relationships". A UTF-16
word/_rels/vbaProject.bin.rels (valid per OPC) therefore hid
vbaData.xml, and saving the .docm dropped it and its relationship.
parse now transcodes UTF-16 LE/BE with a BOM to UTF-8 (lossy, bounded
at 1.5x, never panics on odd lengths or unpaired surrogates), so every
part benefits. Unreadable relationship parts are now logged rather than
silently treated as empty; the macro project is still kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web Save keeps the opened Word format (.docm stays .docm)

Browser Save (file.save without a path) always downloaded
<stem>.docx, so an opened .docm/.dotm was written as a plain .docx and
lost its macros on an ordinary Save. Save and the Save As suggestion
now default to the opened file's extension when it is a Word package
(.docx/.docm/.dotx/.dotm, via engine io::is_word_package), else .docx.
Macros are only dropped on an explicit conversion to a macro-free name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DOCX: carry every part a VBA project relates to, so signatures survive

A signed .docm/.dotm lost its VBA signature on save: the reader kept
only vbaProject.bin and vbaData.xml, and the writer rebuilt
vbaProject.bin.rels with the wordVbaData relationship alone. Under a
"signed macros only" policy the macros then stop running, although the
project bytes (which the signature covers) were unchanged.

Generalised instead of special-casing signature types: the reader now
keeps every internal part vbaProject.bin.rels points to (VBA data, the
legacy/Agile/V3 signatures, anything else) as opaque bytes, with its
relationship type and its content type from [Content_Types].xml, in a
small manifest in Document::passthrough. .docm/.dotm re-emit them all;
.docx/.dotx still drop the whole VBA set. Bounded at 64 parts; targets
outside word/, relationship parts, the project itself, missing parts
and parts the writer produces itself are skipped, never shadowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DOCX: match VBA part names case-insensitively; keep colon names relative

Two edge cases in carrying a VBA project's related parts:
- The reader's word/ prefix check was case-sensitive, so a relationship
  to /WORD/vbaData.xml (the same part under OPC's case-insensitive part
  names, and found by Package::get) was skipped and lost on save. The
  directory is now matched case-insensitively and normalised to word/,
  the name the writer uses.
- Stripping word/ from /word/sig:legacy.bin gave Target="sig:legacy.bin",
  which parses as a URI scheme rather than a package part. A target
  whose first segment has a colon is now written as "./sig:legacy.bin";
  ordinary names keep the plain form Word already accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:34:52 -04:00
Brandon ThomasandClaude Opus 5.5 72262a2ea9 M3/M6/M12: PDF export (krilla), TXT/MD/HTML/RTF/ODT import+export, citations & bibliography, index, table of figures, cross-references, tools (repeat, macros, AutoCorrect, compare, accessibility, inspector, protection, versions, Quick Parts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:57:36 -04:00
Brandon ThomasandClaude Opus 5.5 c29182e8bc M1: DOCX read/write (OOXML) wired into open/save; floating objects; picture & shape tools; mail merge, envelopes, labels; contextual spacing; export without proofing marks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:46:40 -04:00
Brandon ThomasandClaude Opus 5.5 b8dd46ccb5 M0.7: engine — session, undo, 200+ commands, Word feature catalog, samples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:47:26 -04:00