Commit Graph
81 Commits
Author SHA1 Message Date
aiongg 04b3881e3e Integration: track t06
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
2026-10-22 01:47:34 +00:00
aiongg 16d1c8a933 Integration: track t05
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:47 +00: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 61570835bc Merge PR #3156: Watermark: the text fills its shape's box, as the reference app's textpath does
crates/docx/src/read/story.rs: main's vml_opacity with the PR's box size.
crates/pdf/src/tests.rs: both sides' tests kept; the glyph_frames helper both #3155 and #3156 add stays once.
2026-10-22 01:24:10 +00:00
aiongg e311bd410f Merge PR #3155: Text: scaled letters (w:w) are stretched, not spaced out
crates/pdf/src/tests.rs: both sides' new tests kept.
2026-10-22 01:23:50 +00:00
aiongg 57205e310a Merge PR #2392: PDF export: lam-alef drawn as two glyphs at the same x extracts in reading order (#2310)
ROADMAP.md: main's status lines, with this change added to the newest.
docs/file-format-parity.md: main's status line with this change added; PDF export row keeps main's outline note and adds the lam-alef note.
2026-10-22 01:23:07 +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 2df0803aea Merge PR #2099: fix(pdf): shapes with a description are figures in tagged PDF
crates/layout/src/display.rs: main's Paint path fill and fill.as_ref() with the PR's Option<GraphicKind> figure and described-shape figure.
crates/pdf/src/lib.rs: main's holds_picture and rotated_pictures arm kept beside the PR's figure_alt.
crates/pdf/src/tests.rs: union; the PR's shape test builds its fill with Fill::solid (main's shape fill type).
2026-10-22 01:22:37 +00:00
aiongg 54d7597afc Merge PR #3157: Borders: dotted, dashed and dash-dot borders take the reference app's dash patterns
crates/layout/src/display.rs: border_stroke maps Dotted, Dashed and DotDash to the PR's Border* strokes and keeps main's Triple; Stroke::dashes lists Triple as undashed.
crates/pdf/src/tests.rs: kept both new tests.
2026-10-22 01:21:40 +00:00
aiongg 05c34486f5 Merge PR #2086: fix(export): pictures marked decorative are skipped by screen readers 2026-10-22 01:19:47 +00:00
aiongg 81fbf9fb77 Merge PR #3322: wasm: use the browser clock only on wasm32-unknown-unknown, not on WASI 2026-10-22 01:19:46 +00:00
aiongg 4da4ced7ce Merge PR #1915: Draw EMF text records (EXTTEXTOUTW) in metafile pictures 2026-10-22 01:19:40 +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
6b01aa967f Shape fills: gradients and transparency (#1500) (#1605)
* #1500: shape fills keep gradients and transparency (model, DOCX read/write)

A shape's, text box's and canvas's fill is now a `Fill` (solid with
transparency, or a gradient kept as the file states it) instead of one
opaque colour, so gradients and transparency survive opening and saving.

- doc: `fill::Fill`/`Gradient` with sanitizing for hostile input; `.json`
  keeps writing opaque solid fills as `[r,g,b]` and reads both forms;
  `ShapeExtra::outline_fill` keeps an outline gradient.
- docx read: `a:solidFill` with `a:alpha`, `a:gradFill` (stops, `a:lin`,
  `a:path`/`fillToRect`, `tileRect`, `rotWithShape`, `flip`), theme fill
  styles through `a:fillRef` 1-999/1001+ with `phClr`, `a:grpFill`,
  `satMod`, outline gradients, VML `v:fill` gradients and opacity, canvas
  backgrounds.
- docx write: `a:solidFill` + `a:alpha`, `a:gradFill` as read.
- engine/docbin/layout: call sites; drawing still uses the main colour
  (next commit draws gradients).

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

* #1500: draw shape gradients and transparency (layout, render, PDF)

- layout: WordArt's paint becomes a shared `display::Paint` (solid,
  linear, radial); `paint_for` lays a fill over the shape's frame
  (linear at its angle, `scaled` honoured; path gradients circular from
  their focus to the farthest corner). Shapes, freeforms and canvas
  backgrounds use it.
- render: one `set_paint` for shapes, paths and WordArt; radial
  gradients; soft edges fade the real fill.
- pdf: axial and radial shadings with per-stop opacity.
- Review fixes: nested groups without a fill pass theirs on to
  `a:grpFill`, text-box contents don't take a group's fill, `phClr`
  transparency only on `phClr` colours, VML focus ends consistent and
  opacity clamped, `rotWithShape`/`scaled` written only when set,
  Shape Outline clears an outline gradient, shared stop sanitizing.
- docs: file-format, layout parity, gaps #1, roadmap progress log.

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

* #1500: shape.fill sets transparency and gradients on every selected shape

- shape.fill (now cmd/shape_fill.rs) takes {"color", "transparency"?} or {"gradient": {stops,
  type, angle, focus, rotateWithShape}}; a colour alone keeps the transparency; values are
  sanitized; applies to every selected shape, text box and canvas as one undo step.
- Formatting (shape.fill, shape.effects) keeps a Shift+click multi-selection, as in Word.
- view.formatShapePane toggles the Format Shape pane.
- Session::undo_mark/merge_undo_since: a pane drag that applies as it goes undoes in one step.
- Helpers for the pane: params(fill), stop add/remove/move, directions, variations and
  presets (variation and preset colours provisional until compared with Word).
- Catalog: Shape Fill Gradient, Format Shape Pane.

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

* #1500: Shape Fill › Gradient and the Format Shape pane

- Shape Fill › Gradient: No Gradient, Light and Dark Variations of the shape's colour, More
  Gradients… (opens the pane).
- Format Shape pane (right): No fill / Solid fill (colour, transparency) / Gradient fill
  (presets, type, direction, angle, stops bar with add/remove/drag and arrow keys, per-stop
  colour, position, transparency, rotate with shape). Drags preview live, one undo step.
- Right-click › Format Shape… on shapes, text boxes and canvases.
- Gradient swatches have accessible names; new labels translated (own wording, clean room).

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

* #1500: preset gradients are our own design (clean room)

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

* #1500: Gradient variations, starting gradient and directions as Word 365 does them

Observed in Word 365 (behaviour and values only):
- Shape Fill › Gradient: a No Gradient swatch, then 13 Light and 13 Dark Variations in a
  grid five wide, in Word's order; light stops tinted 66/44.5/23.5% with saturation ×1.6,
  dark shaded 30/67.5/100% with ×1.15; linear ones scaled; corners with their tile rectangle.
- Format Shape › Gradient fill starts from the theme's first accent (four stops, 0/74/83/100%).
- Corner directions in the pane get the same tile rectangle; Add/Remove sit by the stops bar.
- shape.fill carries "scaled" and "tile"; docx exports color_modified (lumMod, lumOff, tint,
  shade, satMod) so variations use the reader's own colour maths.

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

* #1500: review fixes for the shape fill ribbon controls

- Gradient variations are built from the colour the shown variation came from, so picking
  variations in turn, or No Gradient, doesn't drift the colour.
- A drag's undo mark is forgotten while the Format Shape pane is closed (a stale mark could
  merge later undo steps).
- Dragging on the stops bar away from a handle no longer moves the selected stop; the arrow
  keys are taken by the bar (no focus moves or shape nudges).
- Direction and Type changes keep a linear gradient's "scaled" and an imported tile
  rectangle; only a corner direction sets its tile.
- Colour modifiers (lumMod, lumOff, tint, shade, satMod) live on Rgb in the doc crate, shared
  by the DOCX reader and the gradient builders.
- Previews build their stop list once per swatch, not per vertex.
- Translations for the pane's hint, direction and preset names.

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

* #1500: adapt to main: caret contrast samples gradient fills; upstream tests use Fill/Paint

- Paint::at gives a paint's colour and opacity at a point; the caret's background sampling
  (#299) uses it for shape fills, now gradients or transparent, and the Format Shape
  previews use it too.
- Upstream tests (#886 shapes survive saving, render) build fills as Fill/Paint.

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

* #1500: adapt to main after the rebase: VML presets, rounded corners, new catalogs

- vml_fill takes the shape's shapetype: a VML preset shape (#1377) takes its fill, opacity and
  gradient from its shapetype when it states none of its own, as it already took its colour.
- Rounded-rectangle corners (#1476) and the gradient paints meet in render, PDF and the caret's
  background sampling; ShapeExtra keeps both `corner` and `outline_fill`.
- Tests that landed on main since build fills as Fill/Paint (VML presets, cover pages, VML
  freeforms, path arrowheads, PDF and render shapes).
- The 40 Shape Fill / Format Shape strings in the hr, ko, sv and vi catalogs, which main
  gained since.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: docs: shape gradients and transparency, Shape Fill › Gradient and the Format Shape pane

Re-applied on main's docs after the rebase: progress log, file-format, layout and UI parity
rows, gaps #1, status lines and revision history; parity checklist regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: a theme picture fill falls back on the reference colour; VML freeform opacity note

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: adapt to main after the second rebase: VML shapetype tests compare Fill; parity checklist regenerated

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: one VML fraction parser; a preset takes its gradient from its shapetype

Review notes: story.rs's watermark/freeform opacity reuses shape_style's vml_fraction (clamped at the callers) instead of a second copy; a test pins a VML preset whose v:fill gradient comes from its v:shapetype, and the 65536ths form of a fraction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: adapt to main after the third rebase: the shape duplicate preview test fills with Fill

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: review — harness test: right-click a shape, Format Shape… opens the pane and keeps the shape selected

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1500: review — theme keeps every fill style a fillRef can reach (999 per list), still bounded

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-10-21 19:12:26 -04:00
c02ceef673 Borders: triple borders draw as three lines on screen and in PDF (#1740) (#2210)
* #1740: triple borders draw as three lines on screen and in PDF

Triple borders (tables, paragraphs, the ### AutoFormat line) were drawn as
double lines. Layout now emits a Triple stroke; the renderer and the PDF
exporter draw three thin lines across the line's direction, so vertical
table borders get three side-by-side lines too.

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

* #1740: rebase onto main — triple border strokes use the line's f64 ends

Main now converts the line ends to f64 once and draws through a shared
offset-line closure; the triple stroke uses both instead of dereferencing
the shadowed values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1740: review — PDF triple-border test checks the three strokes are distinct and 3 widths apart

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-10-21 18:32:33 -04:00
Alex Wine f4299ff644 wasm: use the browser clock only on wasm32-unknown-unknown, not on WASI
`now_ms` in layout, `now_unix` in engine, formats and pdf gated
`js_sys::Date::now()` on `target_arch = "wasm32"`, which also matches
wasm32-wasip1 and wasm32-wasip1-threads. There js-sys has no host to call,
so it panics ("cannot call wasm-bindgen imported functions on non-wasm
targets"), and since `layout()` times itself with `now_ms`, every document
laid out on WASI aborts. `std::time` works on WASI, so these now use the
browser clock only on wasm32-unknown-unknown, the gate wordcraft-docx
already uses.
2026-10-20 12:11:57 -06: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
rabisandClaude Opus 5.5 0cca2f7d21 #1899: a picture cropped to a sliver is drawn at the size the file gives
A crop that kept under 5% of a picture's axis was widened to 5%, so a
header bar cropped to 2.7% of a page-sized WMF came out half as tall and
the text box over it fell below it. The rasteriser, the PDF writer and
the metafile placement now share wordcraft_metafile::crop_window, which
keeps down to 0.1% of an axis and keeps junk crops finite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:29:15 +02:00
Frédéric WeilandClaude Opus 5.5 912b98c833 Watermark: the text fills its shape's box, as Word's textpath does
Word's text watermark is a VML WordArt shape (`PowerPlusWaterMarkObject`)
whose `v:textpath fitshape` stretches the text over the shape's box (its
`style` width and height), centred on the margins and, when diagonal,
turned by `rotation:315`. WordCraft ignored the box: it drew the text
70 % of the page diagonal wide (half of it on screen), centred on the
page and along the page diagonal (55° on A4, not 45°). A "BROUILLON"
watermark in a 515 x 104 pt box came out about 1.4 times as large as in
Word.

- doc: `Watermark::size`, the shape's box (`None`: Word's default, half
  the page diagonal across, as high as the text's length makes it);
  `Watermark::frame` gives the box for a page, shared by the writer and
  the renderers.
- docx: the box is read from the shape's style and written back; a box
  equal to the default for the document's page reads as `None`, so a
  watermark made by Design > Watermark keeps following the page.
- pdf, render: the text's advance spans the box's width, its em the
  box's height, the em's foot (the font's win descent below the
  baseline) on the box's bottom; the box centred on the margins,
  turned 45° counterclockwise when diagonal.

Checked against Word for Microsoft 365 (ink bounds, pt, left top right
bottom; fonts here: Liberation Sans for Arial and Calibri, Liberation
Serif for Times New Roman):

  BROUILLON, Calibri, 515.4 x 104.1, 315°:
    Word 93.6 222.7 489.1 619.2; before 60.5 110.4 535.7 732.5;
    after 90.7 214.1 495.8 618.7
  SPECIMEN, Arial, 400 x 100, 315°:
    Word 136.3 261.6 455.0 580.3; before 66.7 108.5 539.0 727.7;
    after 137.8 255.4 454.6 573.1
  HXH, Arial, 300 x 100, flat: Word 158.9 378.2 436.3 449.8;
    before 105.1 353.8 490.1 489.6; after 158.9 380.2 435.8 449.3
  HXH, Times New Roman, 300 x 100, flat: Word 149.8 383.0 445.0 449.3;
    after 151.2 383.5 443.5 449.3
  COPIE, Calibri, 300 x 80, flat: Word 153.1 387.8 441.1 440.2;
    before 96.0 373.9 500.2 469.0; after 152.2 387.4 443.5 444.5

Tests: a watermark with a box of its own is written and read back with
it, Word's default box reads back as none; in the PDF, the text of a
300 x 80 pt watermark is 80 pt high, stretched across, starting at the
box's left edge on the margins' centre line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:31:10 +02:00
Frédéric Weil | FSSPXandClaude Opus 5.5 a683260abf Borders: dotted, dashed and dash-dot borders take Word's dash patterns
Paragraph, cell and table borders were dashed, but not with Word's
patterns: dots were one width on and two off (Word: one and one),
dashes four on and two off (Word: three and one), and dash-dot was
drawn as plain dashes.

- layout: borders use their own strokes (`Stroke::BorderDotted`,
  `BorderDashed`, `BorderDotDash`); `Stroke::dashes` gives every
  pattern in line widths. Underlines and the other dotted lines keep
  theirs.
- pdf, render: lines are stroked with `Stroke::dashes`.

Checked against Word for Microsoft 365 (ink along each border, on
synthetic documents): borders 0.5, 1 and 2.25 pt wide, dotted / dashed
/ dotDash, are 1-1, 3-1 and 3-1-1-1 widths in Word; before 1-2, 4-2 and
4-2; after as Word (2.25 pt dashes: 6.72 on / 2.28 off in both).

Test: in the PDF, a dotted 1 pt border is dashed 1 / 1, a dashed 2 pt
border 6 / 2, a dash-dot 1 pt border 3 / 1 / 1 / 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:08:46 +02:00
Frédéric WeilandClaude Opus 5.5 94d938fa63 Text: scaled letters (w:w) are stretched, not spaced out
Character scale (`w:w`, Format > Font > Advanced > Scale) widens or
narrows the letters themselves. WordCraft multiplied the advances but
drew each glyph at its natural width, so text at 200 % looked letter-
spaced (pdftotext even split it into words) and text at 50 % overlapped.

- layout: `Draw::Glyphs` carries `x_scale` (1 = 100 %), the run's scale
  for its text and its line-end hyphen (whose advance is scaled too);
  list labels, tab leaders, ruby, equations and shape text keep 1.
- pdf: a scaled run is drawn under a horizontal scale from its first pen
  position, the positions brought back so each glyph lands where the
  layout put it; the text and its extraction are unchanged.
- render: each glyph's outline is stretched across from its pen
  position.

Checked against Word for Microsoft 365, "HIHIH" in Arial 20 pt
(Liberation Sans here), right end of the word (pdftotext) and ink:

  200 %  Word 179.78, ink 73.92..176.52; before: split in three words,
         ink to 163.68; after 179.74, ink 74.04..176.52
  150 %  Word 152.19; after 152.52
   50 %  Word 97.76; after 98.07
  100 %  unchanged

Test: in the PDF, letters at 200 % are drawn twice as wide and as high,
at pen positions twice as far apart; at 100 % unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:02:49 +02:00
MyAIPluginsandClaude Opus 5.5 322b3d765c test(pdf): a described shape is exactly one Figure
The test checked that the description is there, so a Figure written twice
would still pass. It now also counts the alt text entries.

Suggested by the CodeRabbit review of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:36:37 +02:00
MyAIPluginsandClaude Opus 5.5 d16dd5cd72 fix(pdf): tag each picture in a rotated group on its own
A rotated inline group is drawn inside one rotation. Its tag was chosen for
the whole group: an artifact as soon as one member was decorative, so a
picture next to it lost its Figure and alt text; otherwise one Figure with
the first picture's alt text. The group is still turned as a whole, and each
picture in it is now tagged on its own: decorative ones as artifacts, the
others as Figures with their own alt text.

Found by the CodeRabbit review of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:36:37 +02:00
MyAIPluginsandClaude Opus 5.5 73d159b6c9 fix(pdf): shapes with a description are figures in tagged PDF
A shape with a description (Alt Text) was exported as an artifact like any
other shape, so screen readers skipped it and the description was lost: an
arrow or an icon drawn with shapes that means something was invisible to them.

A shape that has a description, is not marked decorative and shows no text is
now a Figure with its description, inline (rotated too) and floating. Shapes
without a description and decorative shapes stay artifacts. A text box keeps
its frame as an artifact and its text read as paragraphs: its description would
replace the text, so it isn't used. The rule lives in `shape_figure_alt`.

Draw::Figure's `kind` becomes an Option (None for a shape), and Placed::Shape
carries the description for floating shapes.

Not covered: a group's own description (its members are placed one by one),
and shapes inside a Drawing Canvas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:52:55 +00:00
MyAIPluginsandClaude Opus 5.5 8664afe58f fix(export): pictures marked decorative are skipped by screen readers
A picture marked decorative (Alt Text > Mark as decorative, `adec:decorative`
in DOCX) was still exported as content:

- tagged PDF: a Figure with its alt text, or with "picture" when it had none,
  inline and floating alike. PDF/UA expects decorative content to be an
  artifact, so screen readers skip it. Draw::Image now carries the mark, and
  the exporter tags a decorative picture (inline, rotated or floating) as an
  artifact instead of a Figure.
- HTML and Markdown (and ODT, which reads the same model): the description,
  which the document keeps when a picture is marked decorative, was written
  as its alt text. A decorative picture now exports with an empty alt text
  (alt="" in HTML), the WCAG way to say "skip this".

Pictures not marked decorative are unchanged. Charts and diagrams are left as
figures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:52:55 +00:00
c91c1793a3 #2108: File › Print scales pages to another paper size (#2699)
* #2108: File › Print scales pages to another paper size

Scale to paper size on File › Print (Word: Pages per sheet › Scale to
paper size): No Scaling or any Layout › Size paper (Letter, Legal, A4, …).
Every page is scaled to the paper and centred, the paper turned to the
page's orientation, in the preview, the print job and Save these pages
as PDF. Native jobs also ask the spooler for that media. The document's
page size is unchanged. New `paper` param on ui.print/ui.printSubmit and
file.printPdf; unknown papers are errors.

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

* pdf: bookmark destinations from #2364 use the scaled Fit too

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:48:17 -04:00
divyanshu2074andBrandon Thomas 5977112a0d render, pdf: fix vertical double and wave border rendering (#2113)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:34:58 -04:00
Wade 999e2ae896 PDF tests read ActualText spans, so RTL extraction no longer depends on the installed Arabic font (#1773) (#1973) 2026-10-18 09:37:56 -04:00
Fin WarmanandBrandon Thomas fc08b0e4f6 Fix PDF outline targets after leading page breaks (#2484)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:56:29 -04:00
341f370368 fix(pdf): scale bookmark destination coordinates with page Fit and support case-insensitive bookmark lookup (#2364)
* fix(pdf): scale bookmark destination coordinates with page Fit and support case-insensitive bookmark lookup

* cargo fmt

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-18 04:45:06 -04:00
3787af0a67 Rendering and PDF check image limits before allocating (#1321) (#2001)
* WIP for #1321

* Images: check size budgets before decoding and cap page rasters (#1321)

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

* Images: charge decode peak, valid-CRC header tests, cap text-line rasters (#1321)

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:42:17 -04:00
2e062168f0 Text effects: legacy outline, shadow, emboss and imprint are drawn (#2534) (#2548)
The legacy run properties w:outline, w:shadow, w:emboss and w:imprint were
read, resolved and saved but never drawn. Layout now turns them into text
effects (hollow outlined letters; a sharp grey copy down-right for shadow
and emboss, up-left for imprint, as Word's PDFs show them) and draws them
through the existing WordArt path, so screen and PDF both get them and the
PDF keeps the text. WordArt w14 effects on the same run take precedence.

Text drawn from outlines keeps synthetic bold (a widened outline, or one
in a solid fill's colour and transparency). Colour glyphs (emoji, no
outlines) and top-to-bottom runs with upright characters are drawn as
plain glyphs, and tracked insertions and deletions keep their legacy
effects.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:50:15 -04:00
da11d6634b Shapes: preserve DrawingML outline dashes (#2661)
* Shapes: preserve DrawingML outline dashes

* Shapes: scale dashes to effective path width (#380)

* Keep the outline dash with noFill; cap the raster dashed stroke width at 200 pt

Addresses the two CodeRabbit review notes.

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

* Use clamp for the dashed stroke width (clippy)

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-18 03:46:35 -04:00
Fin Warman fc15682fb0 Preserve REF field hyperlinks in PDF exports (#2597) 2026-10-18 03:21:29 -04:00
rabisandClaude Opus 5.5 2ffdf353a1 #2310: PDF: lam-alef drawn as two glyphs at the same x extracts in reading order
Some fonts (the static Noto Naskh Arabic builds) draw لا as a lam and a
zero-advance alef with their own clusters at one x. visual_order kept the
tie in logical order, so the right-to-left run's ranges didn't decrease,
krilla dropped /ReversedChars and السلام extracted as السالم. Ties between
clusters now go in reverse text order when right to left (text order
otherwise).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:56:17 +02: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
7af9748ace M1: fields keep w:dirty through saves; computing a result clears it (#877) (#946)
A field begin marker flagged w:dirty by a producer that couldn't compute
its result (pandoc-style TOC placeholders) lost the flag on save, so Word
no longer refreshed the field on open. Mirror w:fldLock end to end:
FieldState, InlineObject::Field/FieldStart, the reader (fldChar and
fldSimple) and the writer (buffered, range and TOC-spanning begins).
Update Field/Update Table clear it on the fields they compute, like
Word's F9; untouched fields keep it. Round-trip and engine regression
tests, incl. a pin for #878 (imported TOC placeholder replaced in place,
fixed earlier by #617).

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:27:25 -04:00
Fin WarmanandBrandon Thomas 6528f972db Shapes: draw flowchart OR spokes (#1644)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 21:21:49 -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
NetterSilverandClaude Haiku 5.5 6931b8db2a Draw EMF text records (EXTTEXTOUTW) in metafile pictures
Fixes #1764

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:50:12 +02:00
fee9b984eb Tables: text running past an exact-height row is cut off (#1617)
* Tables: text running past an exact-height row is cut off

A row with an exact height (w:trHeight w:hRule="exact") keeps that
height whatever its cells hold. Word cuts the text that doesn't fit at
the cell's edge; WordCraft drew it over the rows below, so a certificate's
identification block came out as overlapping lines.

The cell is marked as clipped in layout (Placed::Cell clip), and display
draws its lines inside that cell's clip, on screen and in PDF. Hit
testing is unchanged: the lines stay where they are in the layout.

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

* Tables: keep row clipping compatible with Rust 1.90

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 20:15:39 -04:00
90d8844e44 Shapes: a right triangle keeps its right angle (#1599)
DrawingML's rtTriangle was read as an isosceles triangle, so a cover
page's white corner cut-outs pointed up into a peak instead of cutting a
V into the picture above them. It is now its own shape kind, drawn with
the right angle at the bottom left, wrapped and glued along that outline,
and saved back as rtTriangle. The .doc reader maps msosptRightTriangle (6)
to it too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 19:51:13 -04:00
2c5249d13e All Caps draws ß as SS, as Word does (#1581) (#1583)
* #1581: WIP

* #1581: All Caps and Small Caps draw ß as SS, one character for the caret

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

* #1581: exact caret tests, small-caps scaling, symbol runs never expand

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

* Docs after rebasing on main

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 19:50:23 -04:00
Jeremy YaoandClaude Fable 5.1 c3bdcd73b4 Harden against hostile-document DoS and non-web link export (#1235)
* Harden against hostile-document DoS and non-web link export

A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.

- layout: a table repeats its header rows on every page it spills onto, so
  many tall header rows paginate to O(headers x pages) placed items. Cap the
  total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
  to rows x 64 covered cells, and `model::table` pre-allocated a full
  rows x cols grid only to clear it. Cap rows at MAX_ROWS before
  insert_covered, bound its total work, and build the grid template with one
  row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
  points, making de Casteljau flattening quadratic. Cap points per segment.
  3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
  the same backing text many times. Hold the total characters materialised to
  twice the WordDocument stream size (a ceiling no real file reaches, since
  each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
  limit, so a tiny file declaring a huge image could allocate before the size
  check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
  is_safe_href (as HTML/Markdown already do), and extend it to reject
  protocol-relative (//host) and UNC (\host) references. Keeps file:,
  javascript:, ms-msdt: and UNC targets from being written into exported RTF,
  ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
  Windows).

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

* Align picture-decode limits with the bounds already in force

Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 18:00:35 -04:00
9f7330664b Word forms keep their legacy form fields and rows' skipped grid columns when saved (#1184)
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)

* Tests: field initializers name the new form field

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:58:07 -04:00
cf9a4ff7fa STYLEREF fields show each page's result in headers and footers (#775) (#882)
* #775: STYLEREF fields in headers and footers show each page's result

Running headers/footers with STYLEREF (e.g. the current chapter title) are
evaluated per page at layout time (ECMA-376 §17.16.5.59): the page's first
paragraph of the style (\l: last), else the nearest before, else after.
Style names/ids, heading levels (STYLEREF 1), character styles, and the
\n \r \w \s \t \p switches. Body STYLEREF fields update with the other
fields. STYLEREF added to Insert › Quick Parts › Field with its options.

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

* #775: STYLEREF error text in our own words

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

* Review fixes for #882: translate the STYLEREF Field dialog strings

Adds the style picker label, the six STYLEREF switch labels and the
STYLEREF description to all 18 interface catalogs.

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

* STYLEREF: reuse the style index while the body and styles are unchanged

Headers with STYLEREF rebuilt and hashed the body's style index on every
layout pass (echelon's follow-up note on #882). The layout cache now keeps
the index with the body blocks it came from (shared, so an edited block is
a different pointer) and the styles/numbering hash, and rebuilds it only
when either changed.

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

* vi: STYLEREF field 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:56:47 -04:00
e29fbdb0bf Page background: picture and texture fills are drawn and kept on save (#999) (#1036)
* Page background: picture and texture fills are drawn and kept on save (#999)

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

* rustfmt

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 16:03:32 -04:00
0a2b429b05 fix(pdf): inline pictures keep their alt text in tagged PDF (#1502) (#1526)
* fix(pdf): inline pictures keep their alt text in tagged PDF (#1502)

Inline pictures were tagged as Figure with no alt, so the exporter fell
back to "picture" and screen readers announced every inline image that
way, while floating pictures kept their alt text.

Draw::Image now carries the picture's alt text from the InlineObject, and
the Placed::Lines branch of the PDF exporter passes it to the Figure tag,
including for rotated inline pictures. A picture without alt text still
falls back to "picture".

Adds a regression test in crates/pdf/src/tests.rs.

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

* rustfmt

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 14:45:18 -04:00
52e56f8d76 PDF export: All Caps text extracts correctly (#1473) (#1509)
* #1473: WIP

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

* #1473: list labels and field results in caps extract as shown

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 14:27:23 -04:00
5e8c5b3c4e PDF export: Hebrew and Arabic text extracts in reading order (#1383) (#1508)
* #1383: WIP

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

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

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 14:07:08 -04:00