74 Commits
Author SHA1 Message Date
c524027f34 PDF: pin craft-libs 9f75d3dc for the PNG and PDF input bounds (#4084)
* #1365: PDF: pin craft-libs main, whose Krilla leaves out simple-text (no rustybuzz, ttf-parser)

craft-libs 8f5a2831 turns Krilla's default features off in the shared
workspace and keeps raster-images, so simple-text (the only path to the
unmaintained rustybuzz and ttf-parser) is no longer built. WordCraft
shapes text itself and hands Krilla positioned glyphs. The vendored Krilla
and craft-pdf's writer are unchanged between the two revisions.

Cargo.lock loses rustybuzz, ttf-parser, core_maths,
unicode-bidi-mirroring, unicode-ccc and unicode-properties.

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

* #1365: docs/shared-pdf.md: rewrap the backend features paragraph

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

* PDF: pin craft-libs main, which bounds decoded PNG images in Krilla

* PDF: pin craft-libs 9f75d3dc, with the PDF input bounds of craft-libs#254

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:59:53 -04:00
Martin BallandClaude Opus 5.5 e649cd09da build(i18n): embed the craft-i18n catalogs pinned in craft-i18n.rev
Adds the craft-i18n crate (craft-libs f7cc8a7, storytold/craft-libs#247)
and the build.rs recipe from craft-i18n's docs/integration.md: with
CRAFT_I18N_DIR set, CRAFT_I18N embeds each locale's common.tsv and
wordcraft.tsv (paths via {:?}, never canonicalized); unset, it is empty
and the build is English only; CRAFT_I18N_REQUIRED=1 makes that an
error. craft-i18n.rev pins storytold/craft-i18n be1f053 (LF via
.gitattributes); the flatpak manifest checks out the same commit and
sets CRAFT_I18N_DIR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 22:20:55 +01:00
Brandon ThomasandClaude Opus 5.5 9130181a5f Release 0.10.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:37:16 -04:00
aiongg 0d3b8eb0cb Integration: track t04
crates/formats/src/odt.rs: t01's Body::new/take_blocks kept; take_blocks finishes tables with t04's TableB::finish (column widths). Test files: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:17 +00:00
aiongg 9c3df446ff Merge PR #3247: #1899: a picture cropped to a sliver is drawn at the size the file gives
crates/metafile/src/lib.rs: re-export both TextLayout (#1915) and crop_window.
ROADMAP.md, docs/layout-parity.md: main's status lines kept; the PR's log rows added.
2026-10-22 01:22:55 +00:00
aiongg b0fb2b9386 Merge PR #2101: File: recovery copies and .json files store pictures as base64 (#586 follow-up)
ROADMAP.md: status line, progress log and revision history carry both sides.
docs/file-format-parity.md: status line and change log carry both sides.
2026-10-22 01:22:47 +00:00
Dylan M. Taylor 7f38f41d8c Use chrono instead of hand-rolled UTC conversion (#3427)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil_from_days / days_from_civil copies and the
Excel serial-date, FILETIME and logging/PDF date conversions with chrono as
a workspace dependency (default-features = false, std only, so wasm is
unaffected). Output formats are unchanged and pinned by the existing (and one
new) test. Excel's 1900 leap-year bug and the 1900/1904 systems keep their
explicit special cases; a small proleptic-Gregorian fallback covers years
outside chrono's NaiveDate range so no input changes behaviour or panics.
AGENTS.md now says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic or reshape date output for clocks outside chrono's range

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. logging::timestamp now uses the fallible
DateTime::from_timestamp and reads such a clock as the epoch, as the old
saturating code did.

iso_from_unix_secs fell back to the Unix epoch for timestamps chrono
rejects; it now keeps the proleptic Gregorian date civil_from_days gives,
restoring the pre-chrono result for supported out-of-range u64 inputs.

Years past 9999 are written with {:04} rather than %Y (which prepends a
'+') in logging::timestamp, iso_from_unix_secs, docbin::filetime_iso and
odt::now_iso, so byte-for-byte output is preserved.

filetime_iso gained a docstring; AGENTS.md's chrono bullet now notes the
narrow out-of-range fallback. Tests cover each boundary.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies

The PR kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them. That left calendar arithmetic outside
chrono after all.

Replace every copy with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). This keeps chrono as the only calendar logic
and reproduces the pre-chrono proleptic Gregorian result for every value:

- engine cmd/mod.rs `civil_from_days` (reached by the system clock and by
  `iso_from_unix_secs` for a u64 seconds count past chrono's range).
- engine cmd/controls.rs `days_from_civil` and `days_in_month` (`days_in_month`
  now uses `rem_euclid(400)` since month lengths repeat every 400 years).
- formats sheet.rs `civil` and docx chart.rs `civil` (Excel serials).

Outputs are unchanged: verified the cycle result equals the old formula over a
wide range and at the extremes with a scratch chrono program. New tests pin
accepted inputs past chrono's range on both sides of the epoch, through the
public/lib entry points and the private converters, with values from the old
formula. Removed the AGENTS.md "narrow fallback" exception.

pdf/lib.rs `civil` is left as-is: its only caller is the live system clock and
its year is clamped to 0..9999 by the PDF DateTime, so the epoch fallback past
year 262143 is the accepted live-clock behavior, not a hand-rolled copy.

Tested: cargo test -p wordcraft-engine -p wordcraft-formats -p wordcraft-docx;
fmt --check; clippy --all-targets -D warnings.

* logging/docbin/odt/pdf: keep extended-year dates, no epoch fallback

FredApps (CHANGES_REQUESTED) showed `DateTime::from_timestamp(..).unwrap_or_default()`
changes output: a timestamp the old code converted past chrono's year 262143 range now
collapsed to 1970-01-01 instead of its real proleptic-Gregorian date. The same regression
exists wherever the PR swapped a hand-rolled converter for the epoch fallback. Fix every
such site to keep chrono as the only calendar logic via the 400-year Gregorian cycle
(146_097 days): chrono converts the day within one cycle, whole cycles carry in the year
(i128, saturated into i64). Verified the cycle result equals the old formula across a wide
range and at the extremes.

- apps/wordcraft/src/logging.rs `timestamp`: added `civil_from_days(u64)` (cycle), kept the
  pre-1970 epoch clamp (`duration_since`) and the milliseconds/time-of-day format exactly.
  Pins: UNIX_EPOCH + 10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z (FredApps' example,
  was 1970-...); the checked_add u64::MAX/2 s fixture now asserts 292277026596-12-04T15:30:07.000Z
  instead of the epoch.
- crates/docbin/src/metadata.rs `filetime_iso`: restored a cycle-based `civil(i64)` instead
  of `from_timestamp(..)?` (which dropped the field past chrono's range). A u64 FILETIME caps
  at ~1.83e12 unix s (year 60056), inside chrono's range, so no reachable value changes; pinned
  the largest FILETIME (u64::MAX) -> 60056-05-28T05:36:10Z to lock the extended-year path.
- crates/formats/src/odt.rs `now_iso`: reuse the crate's existing cycle converter
  `sheet::civil` (made pub(crate)) rather than the epoch fallback.
- crates/pdf/src/lib.rs `civil`: cycle converter; year returned unclamped and the caller's
  existing PDF DateTime 0..=9999 year clamp is kept exactly, so month/day/time of a far-future
  clock survive. Pins: civil(10_000_000_000_000)=(318857,5,20,17,46,40),
  civil(-10_000_000_000_000)=(-314918,8,13,6,13,20), civil(253_402_300_800)=(10000,1,1,0,0,0).

engine `cmd/mod.rs::iso_from_unix_secs` already uses the cycle fallback (last round); unchanged.

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:41 -04:00
rabisandClaude Opus 5.5 8e799be97e #1899: wrap outlines and PDF bitmaps follow the same crop window
contour::picture_outline turned the crop into a pixel window on its own,
so a sliver or overlapping crop gave an empty window and Tight/Through
wrapping fell back to the frame while the renderers draw a 0.5% sliver.
It now uses wordcraft_metafile::crop_window, with the upper pixel edges
rounded up. A PDF test covers the bitmap and metafile paths of the export
(CodeRabbit on #3247).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:54:17 +02:00
Brandon ThomasandClaude Opus 5.5 2e6db0bd88 Release 0.9.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:18:50 -04:00
ReesandBrandon Thomas 9726d6f1cb fix(ui): offer Save As for imported Word files (#2448)
* SCRUM-47: offer Save As for imported Word files

* #2445: reject Save As answers for a replaced document

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:26:16 -04:00
c9f3da2fc5 DOCX: mixed Hebrew/Latin keeps its order in Word (runs split by direction, w:rtl and language) (#1543) (#1950)
* WIP for #1543

* DOCX: split mixed-direction runs and mark right-to-left pieces w:rtl so Word orders them as WordCraft does (#1543)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* DOCX: right-to-left pieces of split runs say their language (w:lang w:bidi) (#1543)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* DOCX: no phantom tracked formatting on split runs; typed text doesn't inherit run direction (#1543)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Engine: text typed or inserted anywhere takes its direction from its own letters, not the caret's run (#1543)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:41:59 -04:00
ce38b0c372 #1297: copied equations go in the MathML clipboard formats Word reads (#2614)
A copy or cut of one equation on its own now also puts its MathML in the
platform's MathML clipboard formats, beside the HTML (with MathML, #816) and
the linear text: "MathML" and "MathML Presentation" (UTF-16LE) on Windows,
public.mathml.presentation and public.mathml on macOS. These are the names
W3C MathML 3 section 6.2.4 registers, and Microsoft's "MathML Support in
Microsoft 365" doc says Office checks them first when it pastes math. No
public clipboard format carries OMML. A copy with text around the equation
doesn't offer them, so Word keeps the text.

Not yet verified in Word: the parity docs say MathML clipboard formats, not
an editable paste.


Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:46:15 -04:00
Brandon ThomasandClaude Opus 5.5 f2e15cf8f8 Release 0.8.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:43:17 -04:00
skylar taylor-barrick df28b78de9 refactor(pdf): reconcile shared writer with current main (PDFW)
Refresh #1558 onto fd0bb2fb while preserving the shared writer migration
and current upstream layout, DOCX, field and table behavior.
2026-10-15 11:47:00 -06:00
asystemoffieldsandClaude Opus 5.5 5795259c45 File: recovery copies and .json files store pictures as base64 (#586 follow-up)
Native JSON wrote Document::media and Document::passthrough with serde's
default for bytes, an array of numbers. With a synthetic 27 MiB picture a
recovery copy (compact) was 96.4 MiB and a .json file (pretty-printed,
one number per line) 285.5 MiB.

Each part is now one standard base64 string (doc::byte_store, using the
workspace base64 crate), encoded straight into the writer. Reading accepts
base64 or the old array of numbers, so recovery copies and .json files
written before still open; anything else is an error, never a panic.

Measured (release, same synthetic picture): compact 96.4 -> 36.0 MiB,
pretty 285.5 -> 36.1 MiB, write 252/614 -> 52/57 ms, read 404/533 ->
46/37 ms. A recovery snapshot of a 6,000-paragraph document with the
picture: 99 -> 39 MB, worker time 2.3 -> 0.26 s, start-up scan 1.03 ->
0.14 s.

Tests: byte_store unit tests (round trip, old arrays, hostile values);
recovery_format.rs native_json_stores_bytes_as_base64,
native_json_with_byte_arrays_still_opens, native_json_with_bad_bytes_is_an_error,
ignored measurement native_json_size_with_a_large_picture; desktop.rs
copies_with_byte_arrays_written_before_still_restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:10:23 -04:00
b3b6c451e3 fix(ui): draw Arabic and other non-Latin comment text in balloons (#950)
* fix(ui): draw Arabic and other non-Latin comment text in balloons

Comment balloons and the Comments pane draw comment text and author
names with the interface faces (Inter, Source Sans, CJK), which have no
Arabic, Hebrew, Thai and so on: every such letter was a box. The app now
borrows a document fallback face that covers the comments, as the window
caption does (#840), checked once per document revision.

egui also lays bidi runs and words out left to right, so a right-to-left
comment read backwards. Read-only balloons now wrap the text by word
widths, put each row in display order (Unicode bidi runs, words of a
right-to-left run reversed, brackets mirrored) and set right-to-left
paragraphs against the right edge. Text without right-to-left letters is
laid out exactly as before. The editable fields (the selected balloon and
the Comments pane) keep logical order.

Also adds my contributor entry (sync_github_name).

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

* One contributor entry each

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

---------

Co-authored-by: Mohamed Youssef <ms.rightsteps@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 01:34:11 -04:00
skylar taylor-barrick c3ba9011a2 fix(pdf): preserve shared writer across main updates (PDFW)
Retain the shared writer and its regressions alongside the current PDF text
behavior and the equivalent bounded find-pattern condition.

PR #1558.
2026-10-14 19:18:05 -06:00
3d8f8144d9 File: recover unsaved work after a crash (#518) (#586)
* M10: retain local crash recovery on current main

* i18n: Backstage Recover in every catalog

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-14 17:21:57 -04:00
575c5f98df .doc: open password-protected Word 97–2003 files (RC4, RC4 CryptoAPI, XOR) (#1079) (#1087)
docbin decrypts XOR-obfuscated, RC4 and RC4 CryptoAPI files after verifying the
password against the stored verifier; the engine reuses the .docx password
prompt errors. Written from [MS-DOC] and [MS-OFFCRYPTO]; synthetic tests only.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 17:21:08 -04:00
eb54d88ec0 Mail merge: .xls recipients, legacy encodings, Filter and Sort (#778) (#885)
* #778: .xls and legacy-encoding recipient lists; filter and sort recipients

- Recipient lists: legacy Excel 97-2003 .xls (BIFF8 reader on the cfb crate already in the
  tree); Excel 5.0/95 and password-protected .xls refused with a clear message; a CSV named
  .xls reads as text, an HTML one gets an error. .xls added to the picker filters (desktop,
  web, built-in browser) and drop handling.
- Text lists (and plain-text import) detect legacy encodings when not UTF-8/UTF-16: GBK/GB18030,
  Big5, Shift_JIS, EUC-KR, Windows code pages (encoding_rs + chardetng, MIT/Apache); an
  `encoding` param overrides.
- mailings.filterRecipients / mailings.sortRecipients: field, comparison (equal, notEqual,
  less, greater, lessOrEqual, greaterOrEqual, blank, notBlank, contains, notContains), value,
  And/Or joins; sort keys. Preview, Finish & Merge and labels use only the matching records
  in sort order. Edit Recipient List edits every record and keeps the filter.
- Recipient List dialog: Filter and Sort section and a merge-order column; catalog rows;
  strings in every catalog; docs.

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

* i18n: Hungarian and French strings for this change

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

* Review fixes for #885: total order for recipient sort, Bulgarian strings

- compare_cells now ranks numbers (compared numerically) before text (compared
  case-insensitively), so it is a total order; a column mixing 9, 10 and 1a
  could make slice::sort_by panic. Regression test sorts mixed columns.
- Bulgarian catalog: the 24 Filter and Sort strings.

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

* vi: mail merge Filter and Sort strings

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

---------

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-14 16:57:06 -04:00
e7e4522d0d ODT: read and write tracked changes (text:tracked-changes) (#1517)
* ODT: read and write tracked changes (gap 19)

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

# Conflicts:
#	ROADMAP.md
#	crates/engine/src/tests.rs
#	crates/formats/src/model.rs
#	crates/formats/src/odt.rs
#	docs/file-format-parity.md
#	docs/gaps.md

* ODT tracked changes: deleted paragraph marks don't take the body's insertion; one region per deletion

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:45:07 -04:00
bf2bd69470 HTML import: bold header cells, cell alignment and the region paper (#1381) (#1494)
* #1381: WIP

* #1381: HTML import: bold <th>, cell text-align, region paper for HTML/MD/TXT

Header cells are bold unless their own CSS says otherwise. text-align on a
td/th, or inherited from its tr/row group/table, aligns the cell's
paragraphs; a paragraph's own alignment wins, and a table's align attribute
(which places the table) no longer counts. Files that give no page size
(HTML, Markdown, plain text) open on the region's paper, as a new blank
document does: file.open uses the session's region, wordcraft-cli reads the
system region the way the app does (Region::from_locale_vars, shared with
ui-egui). Tables size to that page.

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

* #1381: the CLI uses the system region everywhere; help mentions WORDCRAFT_REGION

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

---------

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-14 13:58:14 -04:00
Brandon ThomasandClaude Opus 5.5 fc41918814 Release 0.7.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:07:25 -04:00
skylar taylor-barrick 2773295938 PDFW: refresh shared writer on the audited snapshot 2026-10-13 13:11:47 -06:00
skylar taylor-barrick 442a7226e7 PDFW: refresh the qualified shared writer revision 2026-10-13 09:37:58 -06:00
skylar taylor-barrick d1dff91f6f PDFW: use shared writer without reader dependencies 2026-10-12 23:06:05 -06:00
skylar taylor-barrick a381c93e8d Merge pull request #783 from storytold/codex/craft-print-onboarding
Print: select printers and submit jobs through craft-print
2026-10-12 09:46:52 -06:00
Brandon ThomasandClaude Opus 5.5 7dfa6b0ee2 Release 0.6.1: rebuild with the fixed shared release workflow
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:11:12 -04:00
skylar taylor-barrick f0d47b6090 PRINT: integrate current main and expose native print commands 2026-10-12 17:02:36 +02:00
00ed50515e New documents follow the system region: A4, centimetres and decimal comma outside the US (#838) (#857)
* #838: new documents follow the system region (A4/cm/decimal comma outside the US)

- geom: Region (from a locale tag; CLDR Letter and US-measurement territories,
  decimal separator), Unit::format_with; parse_measure takes either separator
  and a Unicode minus.
- engine: Session.region (plain value, en-US headless), Session::units()
  (explicit Measurement units choice wins, else the region's),
  blank_document(); file.new blank uses the region's paper; file.options
  takes "region" and units "auto" and reports unitsFromRegion/region.
  Prefs.units is now Option and saved as measurementUnits (old saved values
  were the inch default, not a choice).
- ui: region from WORDCRAFT_REGION, LC_PAPER/LC_MEASUREMENT/LC_NUMERIC, the
  platform locale (browser on the web) and Windows' home location; length
  fields show and read the region's decimal separator (MeasureExt); Size
  menu lists sizes in the measurement unit.
- Tests: engine new_documents_follow_the_region, geom region/format/parse,
  ui length_fields_follow_the_region. Docs: localization parity, roadmap.

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

* Review fixes for #857: keep a measurement unit chosen before the rename

Settings saved before #838 kept the unit under `editing.units`, defaulting to inches. On load,
a value there other than inches was the user's own choice, so it now carries over to
`measurementUnits` (unless that key is already present); inches, junk and non-strings follow
the region. Test: `a_unit_chosen_before_regions_survives_the_rename`.

Also merged main (ROADMAP.md, engine lib.rs re-exports: `blank_document` and main's
`set_system_locale`). The Windows region code (`GlobalizationPreferences::HomeGeographicRegion`)
was cross-checked with clippy -D warnings for x86_64-pc-windows-gnu.

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

* #838 review: saved region choice, Unix conventions apart, the browser's locale

- File › Options › Advanced › Region: Automatic (the system's) or a chosen region, saved with
  the preferences (`Prefs::region`; `file.options {"region": "pl-PL" | "auto"}`). The session
  keeps the platform's region as `system_region`; `Session::region()` is the one in effect.
  Measurement units gains an Automatic entry (follows the region).
- Unix: numbers follow LC_NUMERIC (else LANG), paper LC_PAPER and measurement LC_MEASUREMENT,
  each on its own (`Region::with_paper_and_measurement`); WORDCRAFT_REGION and LC_ALL still set
  all three.
- Web: sys-locale's `js` feature, so the browser's language reaches the region (and the
  interface language, as its comment already said).
- Runs without preferences (WORDCRAFT_NO_PREFS) stay en-US unless WORDCRAFT_REGION is set.
- docs/localization-parity.md: where the region comes from, the Windows home location vs. Word's
  regional format, and where the decimal separator comes from.

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

* #838 review: rustfmt

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

* #838: rustfmt after merging main

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

* i18n: Hungarian and French strings for this change

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

---------

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-12 08:23:43 -04:00
411ce31de7 Paste keeps other programs' formatting; clipboard pictures on Wayland (#687, #833) (#847)
* #687, #833: paste keeps other programs' formatting; Wayland clipboard pictures

- Paste (Ctrl+V, Home > Paste) reads the system clipboard's HTML, else RTF,
  and keeps its formatting; our own copy still wins while the clipboard holds it.
  Windows CF_HTML headers are understood; oversized or empty payloads fall back
  to the text.
- Merge Formatting inserts the content in the destination's font and paragraph,
  keeping bold, italic, underline and strikethrough (it did nothing before).
- Paste Special lists what the system clipboard holds (HTML, RTF, picture,
  text); Mod+Alt+V opens it even with only a picture there.
- Services gain clipboard_html, clipboard_rtf and clipboard_formats hooks; the
  host reads RTF on Windows (clipboard-win) and Wayland (wl-clipboard-rs).
- Linux: arboard's wayland-data-control feature, so pictures copied in a
  browser or Spectacle reach a native Wayland window (KWin only syncs to
  Xwayland while an X11 window has focus); other picture types are read
  from Wayland directly. A paste that finds nothing usable names the formats
  on the clipboard, or why its picture couldn't be read, instead of "empty".

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

* Review fixes for #847: translate the Paste Special picture strings; Windows/macOS unused import

- `Picture` and `Pastes the clipboard's picture.` translated in all 15 interface catalogs.
- clipboard.rs: `MAX_FILE_BYTES` is used only by the Linux/Wayland module, so it is named
  there; the top-level import was unused (a warning, and a clippy -D warnings error) on Windows
  and macOS.
- Merged main (ROADMAP.md, docs/gaps.md, docs/file-format-parity.md, main.rs services: keeps
  both the clipboard hooks and the default-app hook from #295). Cargo.lock adds only the
  wl-clipboard-rs set.

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

* i18n: Hungarian and French strings for this change

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

---------

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-12 08:18:25 -04:00
b30cda1535 Persist the custom dictionary across sessions (#40) (#796)
* Persist the custom dictionary across sessions (#40)

* Review fixes for #796: session-only Ignore All, desktop-only persistence, capped atomic I/O

- review.ignoreAll no longer adds to (and saves) the custom dictionary: ignored words go to a
  separate session-only list, as Ignore All does in Word.
- Only the desktop app wires the dictionary file. Headless MCP and `wordcraft-cli run` keep
  words in memory, so an agent never reads or changes the person's dictionary (and nothing
  writes outside an automation write root, #769).
- Loading reads through `take(1 MB)`, so a huge file is never read whole; lines that aren't a
  single word (spaces, control characters, invalid UTF-8, over 64 characters) are skipped.
- review.addToDictionary applies the same word rule (proof::custom_word), so a word added is
  never dropped on the next load.
- Saving merges the words on disk first (another process's additions survive; words just
  removed stay out), creates the folder, and replaces the file atomically (temp + rename).
  The file I/O lives in the proof crate, so the engine doesn't touch std::fs itself.
- Tests for each.

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-12 07:23:51 -04:00
Brandon ThomasandClaude Opus 5.5 b7aa7639a9 Release prep: 0.6.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 05:44:42 -04:00
326d695335 Linux: default to XWayland for file drag-and-drop on Wayland desktops (#220)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:49:37 -04:00
d3e2e184c2 Linux: the display theme asks the desktop, not winit (#298)
* Linux: the display theme asks the desktop, not winit

winit reports no system theme on Linux — `ActiveEventLoop::system_theme()` is
`None` there on every desktop — so "Use system setting" never saw the desktop's
choice and fell back to light. A new `desktop_theme` module asks the XDG
Desktop Portal's Settings interface for
`org.gnome.desktop.interface color-scheme` on a thread of its own, so no
D-Bus call is ever made from the frame loop, and the app follows the answer,
including a change made while it runs.

The colour scheme is asked for under its GNOME schema name first, which
GNOME's portal and XFCE's both answer, then under the older freedesktop one,
which KDE answers. A desktop that answers neither leaves the user's own choice
alone, exactly as before. macOS and Windows are untouched: winit answers there.

* Review fixes for #298: one D-Bus connection; watcher started by the app

- The watcher keeps one session-bus connection for its lifetime instead of opening a new
  one every 4 s; a connection whose calls all fail is dropped and reopened on the next poll.
- WordApp::new no longer starts the watcher. The desktop app calls
  WordApp::watch_desktop_theme at start-up, so unit tests, kittest and ui_shot don't spawn
  D-Bus pollers and what they draw doesn't depend on the host's desktop theme (test:
  a_new_app_does_not_watch_the_desktop).
- A change wakes the window (request_repaint), so an idle app follows a theme switch.

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

* Test: name the empty Vec's type (a new dependency adds a PartialEq impl for char)

---------

Co-authored-by: GyuriX <gyurix@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:18:29 -04:00
6801d89065 .doc import: inline pictures, floating pictures and basic shapes (#551) (#558)
* .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>
2026-10-12 03:42:18 -04:00
skylar taylor-barrick a38caa4b15 Print: preserve native workers across upstream platform shutdown changes 2026-10-12 08:37:53 +02:00
c08d9f8598 Accessibility Options: Animation effects setting that follows the system (#791)
* Options: Animation effects setting that follows the system

The app eased wheel scrolling, Ctrl+wheel zoom and its fades whatever the
system's animation setting was: with Windows' Animation effects off, the
page still smooth-scrolled.

File > Options > Accessibility > Animation effects: Use system setting
(the default), On or Off; `ui.animations` for agents; kept in ui.json.

- Off: a wheel notch scrolls the page its whole distance at once, egui's
  own scroll areas get the notch whole instead of eased in, Ctrl+wheel
  zoom steps at once, and egui's fades and scroll-to glides are off.
  Touchpad momentum and the caret blink are unchanged.
- Use system setting asks the host (`Services::animations_enabled`) at
  start-up and when the window regains focus. Windows: the Windows
  Runtime's UISettings, a safe API of the `windows` crate already in the
  build. Web: `prefers-reduced-motion`. macOS and Linux/BSD aren't read
  yet (docs/gaps.md); there it means on.
- The eight new strings are in all eleven interface catalogs.

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

* Animation effects off: zoom steps land finished in one frame

Follow-up to the Animation effects setting: zooming could still look
animated with the setting off.

- Ctrl+wheel zooms in whole steps, each at once. A mouse-wheel notch is
  two steps in its frame, the same zoom as before. Turns that arrive in
  small pieces (a touchpad pinch that Windows reports as Ctrl+wheel, a
  high-resolution or free-spinning wheel) add up to a step of about 10%
  first, where each piece used to zoom a little and the zoom glided.
- A page re-rendered for a zoom step is drawn on the pixel grid in that
  frame. It used to be drawn off the grid, slightly soft, and snap sharp
  a frame later.
- The frame a zoom changes in asks for the next one at once, so the
  status bar's percentage and slider follow in the next frame. They
  could lag until the caret's next blink, up to half a second.

The last two apply whatever the setting.

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

* Animation effects: follow the system's setting on macOS, Linux and the BSDs

"Use system setting" only read Windows and the browser; elsewhere it
meant on.

- macOS: System Settings > Accessibility > Display > Reduce motion,
  through NSWorkspace (a safe call in objc2-app-kit 0.3, already in the
  build).
- Linux and the BSDs: the XDG settings portal over D-Bus (zbus, already
  in the build through AccessKit). Three settings say whether animations
  play, and any one saying no turns them off: the cross-desktop
  reduced-motion preference, GNOME's enable-animations and KDE Plasma's
  animation speed. A thread reads them at start-up and follows the
  portal's change signal. Where no portal answers, or it knows none of
  them, KDE's kdeglobals and GTK's settings.ini are read instead.
- The UI now asks the host every frame instead of at start-up and on
  focus, so a change in the system's settings is followed while the app
  runs. Hosts answer from memory: Windows and macOS are asked again at
  most once a second, the web keeps its media query, and Linux/BSD is
  kept current by its thread.

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

* Animation setting on Linux: ask every setting again when one changes

KDE's portal announces a new animation speed only as the cross-desktop
reduced-motion preference, not under its own kdeglobals name. Keeping
each setting's last announced value left KDE's own one stale: after
going from instant back to normal speed, animations stayed off.

Every setting is now read afresh on any relevant change, and the
settings files' answer stands whenever the portal has none.

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

* Caret glide follows Animation effects; its canvas test turns the setting off, then on

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:24:58 -04:00
95abff4205 MacOS window close (#539)
* MacOS window close

The red close button was aborting inside AppKit. The close button now cancels that in-loop window drop, saves preferences, and quits on the next turn of the main queue the same way Command-Q does.

* Review fixes for #539: remove the control key file before the macOS quit

Merge main. The new close path quits through terminate:/exit, which skips
on_exit, so it now removes the control key file as on_exit does (prefs were
already saved). Clippy-checked for aarch64-apple-darwin.

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-12 02:05:32 -04:00
skylar taylor-barrick 1d0dff03a4 Print: preserve page planning and pass native copy collation to spooler 2026-10-12 06:50:19 +02:00
1b96e04b2c Add fonts at run time: Add Font…, drop a font file, font.add (#582) (#668)
* Add fonts at run time: Add Font…, drop a font file, font.add (#582)

Rebased onto main as one commit (the branch had merge commits from main).

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

* #582: font test no longer depends on the system's fallback font

The FreeBSD runner has no system fonts, so a missing family fell back to
Source Serif 4, the font the test compared against. The test now adds a
renamed JetBrains Mono under a name without Serif/Sans/Mono (which picks a
fallback of that kind), with narrow-letter sample text: monospaced, it
measures unlike any proportional fallback.

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

---------

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-12 00:38:20 -04:00
3fd97c3e58 Complex scripts: shape Myanmar, Khmer and Indic text; caret and line breaks respect syllables (#604) (#652)
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:22:57 -04:00
skylar taylor-barrick 98c457a98d Print: adopt craft-print with native settings and CI 2026-10-12 03:35:23 +02:00
2ffba93b51 WordArt text effects and transforms, and shape connectors (#416) (#481)
* WordArt text effects and transforms, and shape connectors (#416)

- Model: WordArt text effects on runs (fill solid/gradient/none, outline,
  shadow, glow, reflection), text warps on shapes (all DrawingML presets
  kept, adjust values capped), connector kinds (straight, elbow, curved)
  with glued ends, arrowheads and drawing ids (ShapeExtra).
- DOCX: w14:textFill/textOutline/shadow/glow/reflection in w:rPr,
  a:prstTxWarp in wps:bodyPr, wps:cNvCnPr a:stCxn/a:endCxn,
  a:headEnd/a:tailEnd; glued-to shapes keep their docPr ids.
- Layout/render/PDF: text with effects drawn from glyph outlines as paint
  layers (gradients, shadow/glow bands, faded reflection); warped text
  boxes bent along arch up/down, circle, wave, slant, inflate/deflate,
  chevron and triangle; connectors and arrowheads drawn as paths.
- Engine: insert.wordArt makes a floating WordArt text box with our own
  styles; format.textEffects, wordArt.style/textFill/textOutline/transform,
  shape.connect; glued connectors re-route after object edits.
- UI: WordArt style gallery (Insert, Home › Text Effects, Shape Format ›
  WordArt Styles), Text Fill/Outline/Effects menus, Transform previews
  drawn from the warp geometry, connectors in Insert › Shapes, drag a
  connector end onto a shape to glue it.

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

* Fix tests for ShapeExtra, regenerate parity checklist, fmt (#416)

* Regenerate parity checklist after rebase (#416)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 16:51:01 -04:00
589e82e050 Mail merge recipients from Excel and OpenDocument spreadsheets (#334) (#341)
Select Recipients › Use an Existing List… now reads .xlsx, .xlsm and .ods
workbooks besides CSV. A new read-only SpreadsheetML / ODF spreadsheet
reader in wordcraft-formats lists the sheets and turns one sheet into rows
of display strings: shared, inline and rich strings, numbers, booleans,
cached formula results, dates and times as ISO 8601 from the cell's number
format, sparse cells, ODS value types, repeated rows/columns and covered
cells. Zip entries, rows, columns, cells, string lengths and repeat counts
are capped, and nothing recurses.

mailings.recipients takes "data" (base64) and "sheet" and "headers"; a
workbook with several sheets and no sheet answers with the sheet names, and
the app shows a Select Table dialog with "First row of data contains column
headers". The desktop picker, dropped files and the web inbox route
spreadsheets to the recipient list.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:31:25 -04:00
860f4ca260 Tight and Through wrapping follow the object's outline (#354) (#360)
Text now wraps around a Tight or Through object's shape instead of its
bounding box. The outline is the object's wrap polygon (wp:wrapPolygon,
in the 21600-unit space of its frame) when it has one; otherwise it is
derived: a shape's geometry (ellipse, triangle, star, heart... flattened),
the convex hull of a picture's opaque pixels (alpha threshold on a coarse
grid, at most 64 points), or the rectangle. The outline turns and flips
with the object, keeps the left/right and top/bottom distances from text,
and is built once per placed object; each line asks it which stretches of
its band it covers. Through also lets text into gaps inside the outline
that are wide enough.

DOCX reads and writes wp:wrapPolygon (with edited) for wrapTight and
wrapThrough; a shape without one gets its outline written, other objects
their rectangle, since the schema requires a polygon. Square, top and
bottom, behind and in front of text are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:10:48 -04:00
Brandon ThomasandClaude Opus 5.5 e4b7d155f1 Bump version to 0.5.0; add Linux riscv64 release build (#394)
- release.yml: new `linux-riscv64` job ported from GridCraft. Cross-compiles
  wordcraft and wordcraft-cli on ubuntu-24.04 with riscv64 libs from
  ports.ubuntu.com (riscv64 variants of the Linux job's -dev packages),
  packages a tar.gz, and smoke-tests the CLI under QEMU. The draft-release
  job needs it, so its tar.gz is uploaded and covered by SHA256SUMS.
- packaging/linux/package.sh: CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE/EMULATOR
  support (as in GridCraft).
- README and docs/releasing.md list the riscv64 tarball.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:41:30 -04:00
pkibiego 5c970f90bd Bump quick-xml to 0.41: the namespace-declaration DoS was reachable (RUSTSEC-2026-0195) (#363)
`crates/docx/src/xml.rs` parses untrusted document XML through
`quick_xml::reader::NsReader`. In 0.38.4 that is RUSTSEC-2026-0195 (CVSS 7.5): the
namespace resolver pushes a binding for every `xmlns` / `xmlns:*` declaration on a
start tag into an unbounded `Vec`, with no limit on the number of declarations.

None of the reader's existing caps bound that path - MAX_PART (256 MiB), MAX_TOTAL
(512 MiB), MAX_ENTRIES, MAX_DEPTH and MAX_ELEMENTS bound part size, entry count,
nesting and element count, not declarations per tag.

Measured before this change, `wordcraft-cli convert` on a crafted .docx whose
`word/document.xml` is one start tag with 21,845,333 `xmlns:a="u"` declarations:

  508 KB on disk -> 262 MB decompressed (under MAX_PART) -> 1.19 GB peak RSS,
  exit 0: the document opened and converted without any warning.

At the MAX_PART/MAX_TOTAL ceiling that is roughly 2.4 GB of resident memory from
about a megabyte of download, i.e. an OOM kill with unsaved work open. Nothing
panics, so the `Session::run` guard does not catch it.

0.41.0 caps namespace declarations per start tag at 256
(`DEFAULT_MAX_DECLARATIONS_PER_ELEMENT`, raisable through
`NamespaceResolver::set_max_declarations_per_element`) and returns
`NamespaceError::TooManyDeclarations` beyond it. The same fixture now fails in
about a second with a clear error and no namespace allocation:

  wordcraft-cli: malformed XML: start tag declares more than 256 namespace
  bindings; raise the limit with NamespaceResolver::set_max_declarations_per_element

Two API adaptations for 0.41 (0.x bumps are allowed to break):
- `resolve_attribute` moved off `NsReader` on to the resolver:
  `r.resolve_attribute(key)` -> `r.resolver_mut().resolve_attribute(key)`.
- `Attribute::unescape_value` is deprecated in favour of `normalized_value`, which
  also applies XML attribute-value normalization as the spec requires; attribute
  values in a Word part are UTF-8 here (UTF-16 parts are transcoded first).

Regression test: `crates/docx/src/xml.rs::too_many_namespace_declarations_is_error`
asserts a tag with 10,000 declarations is an error. It fails on 0.38.4 and passes
on 0.41.0.

This also clears the second quick-xml advisory, RUSTSEC-2026-0194
(quadratic duplicate-attribute scan). That one was not reachable here - the reader
already passes `with_checks(false)` at xml.rs:257, and a 60,000-attribute tag parses
in the same 65 ms as a 20,000-attribute one - but `cargo audit` is clean afterwards.

Verified: `cargo xtask ci`; `cargo audit` (no vulnerabilities left, the two
unmaintained-crate warnings are `krilla`'s `rustybuzz`/`ttf-parser` and are
unrelated); 32 docx tests plus the malformed-input suite; and 21 crafted hostile
documents through `wordcraft-cli convert` with zero panics before and after.

One residual, unchanged by this commit: the fixture above still costs about 509 MB,
because MAX_PART lets one part decompress to 256 MiB and every part is held in memory
while the package is read. That is the existing, intended bound; what this commit
removes is the namespace allocation on top of it.
2026-10-10 18:04:11 -04:00
4b450f1b00 Open and save password-protected Word documents (#55) (#281)
* Clipboard pane: collect the last 24 copies and cuts and paste any of them again

Copy and Cut now also keep each item (rich fragment plus plain text) in a
session list of up to 24, newest first. Items over ~4 MB (estimated) are
skipped and the list stays under ~24 MB in total; copying the same thing
twice in a row adds it once. The system clipboard is untouched.

New commands: edit.clipboardPane (toggle the pane, also the Clipboard
group's dialog launcher), edit.clipboardItems (list with previews),
edit.pasteClipboardItem {index}, edit.pasteAllClipboard,
edit.deleteClipboardItem {index} and edit.clearClipboard. Pasting goes
through the normal paste path, so it is one undo step and respects Track
Changes. The pane lists the items with a short preview; clicking one pastes
it at the caret, and each has Delete, plus Paste All and Clear All.

Parity: 378 -> 379 of 428 features.

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

* Layout > Arrange > Group: group pictures, shapes and text boxes into one object

Shift+click (or Ctrl+click) adds pictures, shapes and text boxes to the
selection; Group (arrange.group) combines the selected floating objects
into one group object that moves, wraps and resizes as one, with its
members keeping their places and scaling with the group. Ungroup
(arrange.ungroup) splits it back, where the group showed them, and
leaves them selected so Group puts them back together.

- Model: InlineObject::Group with the members' coordinate space (like
  DrawingML a:chExt), so resizing the group scales its members.
- Layout draws the members inside the group's frame; hit testing
  selects the group; text boxes in a group show their text.
- DOCX writes wpg:wgp with a:chOff/a:chExt and each member's a:xfrm,
  and reads Word's groups (including inside mc:AlternateContent and
  nested groups, flattened).
- select.addObject makes multi-selection scriptable (pos or Selection
  Pane index).

Parity 378 -> 379 of 428 (88.6%); floor raised to 88.5%.

Fixes #265

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

* Paragraph: Asian Typography settings, with kinsoku and mid-word Latin wrapping in layout (#269)

Paragraphs gain the Asian Typography flags (line-breaking rules, wrapping
Latin text in the middle of a word, hanging punctuation, compressing
punctuation at the start of a line, and automatic space between Asian and
Latin text and between Asian text and numbers). They inherit through styles,
round-trip through .docx pPr, are set by the new para.asianTypography
command and appear on a new Asian Typography tab of the Paragraph dialog.

Layout applies the kinsoku lists (no line starts with closing East Asian
punctuation or ends with opening punctuation), breaks between any East Asian
characters when kinsoku is off, and breaks Latin words anywhere when word
wrap is off. Text without East Asian characters breaks as before.

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

* Manage Styles: list, preview, delete and show or hide styles

Home › Styles › Manage Styles (styles.manage) opens a dialog from the
Styles pane listing the document's paragraph, character, linked and table
styles, sorted as recommended or alphabetically, with a preview line and a
description (type, formatting it sets, what it is based on). From there:
Modify… and New Style… reuse the existing dialogs and come back, Delete
removes custom styles, and two checkboxes show the style in the Styles
gallery (w:qFormat) or hide it from style lists (w:semiHidden).

styles.manage returns the list for scripts (name, type, builtIn,
inGallery, hidden, basedOn, priority, own formatting). styles.delete now
moves paragraphs, runs and tables that used the style to its base (in
every story), rebases styles built on it and takes a custom linked
character half with it; built-in styles are refused and it is undoable.
New styles.setVisibility {style, gallery?, hidden?} sets the flags, which
round-trip through .docx.

Fixes #266

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

* Create and modify your own table styles, saved in the .docx (#146)

New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.

- engine: table.newStyle / table.modifyStyle with documented params
  (regions take borders, fill, bold, italic, colour, size, font; bandSize;
  para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
  row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
  read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
  style tiles redraw when a style changes.

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

* Table styles: every region, a live preview, delete, custom styles first (#146)

Follow-up to the custom table styles work: the New/Modify Table Style
dialog now covers every region a table style formats, shows what the
style will look like, and table styles can be deleted.

- engine: table.newStyle / table.modifyStyle take lastRow (total row),
  firstColumn, lastColumn and bandedColumns regions with the same props
  as the others. New table.deleteStyle removes a custom table style:
  built-in ones are refused, styles based on it are re-based on its own
  base with its formatting folded in (so they look the same), and tables
  using it fall back to Table Grid. Undoable like any command.
- doc/layout: table styles gain fill and cell borders for the first and
  last column and total row, and fill, text and borders for column
  bands; layout applies them in the standard order (column bands, row
  bands, first/last column, header/total row).
- docx: firstCol/lastCol/lastRow/band1Vert shading and cell borders are
  read and written.
- ui: the dialog's "Apply formatting to" lists all seven regions and a
  small live preview table shows the pending changes; Styles menu gets
  Delete Table Style (disabled for built-ins); the gallery shows the
  document's own table styles first.

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

* Paste pictures from the system clipboard (screenshots, Copy Image, copied files) (#45)

egui's paste event only carries text, so a clipboard holding just a picture
sent no paste at all on Ctrl+V, and the ribbon/context-menu Paste only knew
WordCraft's own copy ("the clipboard is empty").

Desktop: the app reads the clipboard's picture through a new
Services::clipboard_picture (arboard, already in the build via egui-winit):
copied files first, else image pixels. Mod+V whose release arrives without a
paste event, and Paste from the ribbon or menus, insert that picture through
insert.picture (one undo step); text naming copied picture files inserts the
pictures. Pixel sizes are hostile: at most 16,000 px a side and 50 Mpx, the
buffer must match, files at most 200 MB.

Web: a document paste listener reads image files from the paste event when
there's no text and delivers them through the inbox like dropped pictures.

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

* Shape Format: Shape Effects — shadow, glow and soft edges (#275)

Shapes and text boxes get an effects model (outer shadow with colour,
transparency, blur, distance and angle; glow with colour, size and
transparency; soft-edge radius), the shape.effects command (presets,
custom values or null per effect, applied to every selected shape,
undoable, hostile numbers clamped), screen and PDF rendering with the
same banded approximation of a blur, DOCX a:effectLst read/write
(outerShdw, glow, softEdge) with a matching wp:effectExtent, and a
contextual Shape Format tab with Shape Fill, Shape Outline and Shape
Effects (Shadow, Glow, Soft Edges submenus) plus Arrange.

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

* Open and save password-protected Word documents (#55)

Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.

- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
  PasswordRequired / WrongPassword / Encryption errors; hostile files are
  bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
  {"password": string|null} sets or removes the save password (pure, so it
  never lands in Repeat or recorded macros); a document opened with a
  password is saved with it again; file.info reports "encrypted".
  Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
  error), File > Info > Protect Document with Encrypt with Password (typed
  twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
  zeroize.

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

* Table styles: Table Grid and Normal Table can't be deleted even when a .docx marks them custom

A .docx can set w:customStyle on any style, which made Word's built-in table styles look custom
and deletable. table.deleteStyle and the ribbon's Delete Table Style now also recognise them by
id and name, so no table is left on a style that's gone.

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

* Manage Styles: deleting a table style runs Table Design's table style deletion

styles.delete on a table style now calls table_style::delete (#263): its tables take Table Grid,
styles based on it keep their look, and Word's built-in table styles are refused even when a
.docx marks them custom (the Manage Styles list reports them built-in too). Document::restyle no
longer retargets tables' own styles, so there is one place that does.

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

* Paste pictures: Mod+Shift+V (Paste Formatting) no longer pastes a clipboard picture

The key-release fallback that pastes the system clipboard's picture treated every Mod+V release as
Paste. It now requires plain Mod+V or the Paste key (no Shift, no Alt), with a unit test.

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

* Asian typography: test that kinsoku keeps a manual line break before a closing bracket

Manual line breaks end the line by cluster kind, not by break opportunity, so kinsoku's filter
can't drop one before 」; pin that with a layout test.

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

* Encrypted saves: the data-integrity HMAC key is hashSize bytes

MS-OFFCRYPTO §2.3.4.14 makes the HMAC salt as long as the hash (64 bytes for SHA-512); it was
16. Reading already accepted both.

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

* i18n: password dialog and File › Info strings in every catalog

Rows for the Password and Encrypt with Password dialogs, the Protect Document section of
File › Info and the password status messages in zh-hans, zh-hant, ja, pt-br, es, uk, sr and
sr-latn.

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

* Open and save password-protected Word documents (#55)

Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.

- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
  PasswordRequired / WrongPassword / Encryption errors; hostile files are
  bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
  {"password": string|null} sets or removes the save password (pure, so it
  never lands in Repeat or recorded macros); a document opened with a
  password is saved with it again; file.info reports "encrypted".
  Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
  error), File > Info > Protect Document with Encrypt with Password (typed
  twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
  zeroize.

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

* Reland: catalog coverage 390/431 (90.5%) in ROADMAP and target-app parity

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

* Manage Styles: mark table styles with ⊞, which the interface fonts have (▦ has no glyph, #305's check)

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>
2026-10-10 16:16:35 -04:00
11a6ad60a5 Paste pictures from the system clipboard (screenshots, Copy Image, copied files) (#45) (#272)
egui's paste event only carries text, so a clipboard holding just a picture
sent no paste at all on Ctrl+V, and the ribbon/context-menu Paste only knew
WordCraft's own copy ("the clipboard is empty").

Desktop: the app reads the clipboard's picture through a new
Services::clipboard_picture (arboard, already in the build via egui-winit):
copied files first, else image pixels. Mod+V whose release arrives without a
paste event, and Paste from the ribbon or menus, insert that picture through
insert.picture (one undo step); text naming copied picture files inserts the
pictures. Pixel sizes are hostile: at most 16,000 px a side and 50 Mpx, the
buffer must match, files at most 200 MB.

Web: a document paste listener reads image files from the paste event when
there's no text and delivers them through the inbox like dropped pictures.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:54:28 -04:00