Commit Graph
64 Commits
Author SHA1 Message Date
Gabriel Matozo c4e3dc272c Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)
#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.
2026-10-23 11:44:59 -04:00
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 0255a44d9a Merge PR #3389: M2.1: stabilize font and shadow geometry checks
docs/gaps.md: status line keeps main's and adds the PR's entry; main's last log row kept above the PR's new section.
2026-10-22 01:24:20 +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 f9d0295d98 Merge PR #3068: Rendering reuses one render context, so typing starts no threads (#3027)
Doc status lines keep main's and add the PR's entry; log rows unioned: ROADMAP.md
2026-10-22 01:23:41 +00:00
aiongg 6336f7b521 Merge PR #2947: Render: fade dimmed headers and footers as one layer
crates/render/src/lib.rs: imports keep main's Paint and take the PR's page_layers (page_display moves to the test imports).
2026-10-22 01:23:41 +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 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 4da4ced7ce Merge PR #1915: Draw EMF text records (EXTTEXTOUTW) in metafile pictures 2026-10-22 01:19:40 +00: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
Fin Warman cd3944c36e M2.1: check text-shadow geometry with translated masks 2026-10-21 09:06:00 +01: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
dfgodvan 3e5eeccfa2 Render: fade dimmed headers and footers as one layer
While the body is edited, headers and footers are drawn dimmed. Each draw
item got the dimming alpha on its own, so wherever two of them overlap the
alpha was applied twice: a header table's borders came out darker where
they meet and along shared cell edges, nearly black at crossings. The
PDF export and the undimmed view were fine.

page_layers gives a page's draws as two layers (headers/footers, body) at
full strength with the opacity to draw each at. The canvas draws a dimmed
layer inside one opacity layer, as Word dims it, and background_at blends
it the same way. page_display is unchanged for the PDF export and tests.
2026-10-19 14:05:15 +03:00
rabisandClaude Opus 5.5 fba2331bbf #3027: reset test checks the fill rule and paint transform for real
The test scene set even-odd itself before its only path that depends on
it and used solid paints only, so a fill rule or paint transform left
over from the last render could not change its pixels. It now fills
nested squares and a gradient on the defaults first, and checks that a
stale rule or paint transform does change the scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:00:20 +02:00
SlavenDj 76d8347f77 #2892: rules end flat on screen, like the PDF export (#3061) 2026-10-19 02:44:02 -04:00
rabisandClaude Opus 5.5 955d166a69 #3027: renders reuse one render context instead of a thread pool each
Every page, area and icon render built its own multi-threaded vello_cpu
RenderContext, which starts and tears down a rayon thread pool: a few
threads per keystroke while typing. Renders now take one kept context,
reset to their size, and give it back; a render that overlaps another
builds its own, and one that panics drops its context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 08:35:17 +02:00
Max Murphy 00abb59a85 render: keep the legacy font effect test off antialiasing (#2898 x #2899) (#2990)
The shadow copy is solid RGB 128; the old 100-160 grey band also counted the glyph's antialiasing, whose centroid hid the copy's offset with some fonts. This was already failing on origin/main on macOS; count the copy's own grey.
2026-10-18 16:40:22 -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
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
b2b8ad8f52 Font list: entries of tall-ascent fonts are no longer cut off (#2570) (#2598)
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-18 03:21:48 -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
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
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
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
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
0a1f2ce7a8 Canvas: draw colour emoji on screen as the PDF does (#1269) (#1504)
* #1269: WIP

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

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

* #1269: smaller bitmaps are accepted; dark page keeps emoji colours (test)

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:59:25 -04:00
a124d58e5b Dark page: highlights are dimmed so the text on them stays readable (#1252) (#1498)
* #1252: dim highlights on a dark page so the text on them reads

On a dark page (View > Switch Modes) highlight and character-shading fills
were lightness-inverted like every other colour, which leaves saturated
hues such as yellow bright under white text. They are now marked as text
backgrounds (Draw::Fill text_bg) and dimmed toward black keeping their hue
(RenderOptions::ink_bg; yellow -> ~#4a4a10), and text drawn on them
(Draw::Glyphs on_text_bg) takes the lighter of its colour and its inverted
colour (RenderOptions::ink_text), so black and Automatic text draw white
with WCAG contrast >= 4.5 on all 16 highlight colours. Light page, print
and PDF are unchanged.

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

* #1252: text on a dimmed highlight always reaches 4.5:1; lines too

Glyphs and lines now carry the text background they sit on
(Draw::Glyphs/Draw::Line text_bg: Option<Rgb>) instead of a bool. On a
dark page RenderOptions::ink_text takes the lighter of the colour and its
inversion, and falls back to white when that is under 4.5:1 against the
dimmed background (pure blue on a dim olive was 1.34:1). Underlines and
strikethroughs on a highlight use the same rule. The dim constants are
crate-private and documented as assuming the default dark paper grey.

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

* #1252: docs: the 4.5:1 rule for text and lines on highlights

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:53 -04:00
dea3e217fa Shapes: rounded rectangles keep their own corner radius (#1476)
* Shapes: rounded rectangles keep their own corner radius

A roundRect's adj (its corner in 100000ths of the shorter side) is read, kept
on the shape, saved back, and used for the screen, the PDF and Tight/Through
wrapping. Without one the corner stays at 16%.

* Render: test rounded rectangle corners through layout and rendering

* Docs: progress log and file-format parity for rounded rectangle corners

* Render: pass the shape's corner to the caret-background hit test (#299 code from main)

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 13:42:34 -04:00
9fd3490e4c Caret contrasts with the page colour, shading or highlight under it (#299) (#1137)
* #299: caret contrasts with the paper, page colour, shading or highlight under it

The caret was black (white only in Switch Modes' dark page), so it vanished
on a dark page colour, dark shading or a dark highlight. It now takes black
or white by WCAG contrast with the background at the caret
(wordcraft_render::background_at), cached per layout/view/position.

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

* #299: shapes colour the caret's background only inside their outline; look up only when drawn

background_at hit-tests a filled shape's real outline (shape_path), so an ellipse's corner
shows what lies beneath. The caret's colour is worked out only when the caret (or IME text)
is drawn, and the unused Tokens::caret is gone.

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

* #299: rustfmt

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 02:02:34 -04:00
Marco Arnulfo aa91e46753 Shapes: the drag preview shows the shape alone, not what was behind it (#1097) 2026-10-14 01:52:59 -04:00
9d1e207c78 #802: Insert › Picture takes Windows Metafiles and Enhanced Metafiles (#869)
WMF and EMF pictures already drew (crates/metafile) when they came inside a
document, but Insert › Picture refused them ("not a supported image"), and the
file dialog, drag-and-drop and paste lists left them out. A metafile is now
placed at its own frame size (points for placeable WMF and EMF; very large
frames scaled to 22 inches on the longer side) and stored as .wmf or .emf, so a
save writes it as such. render gains metafile_size().

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 11:19:13 -04:00
1350cb959b Font list previews no longer keep every family in memory (#848) (#874)
* #848: font list previews no longer load every family they show

The Font box drew each entry in its own face through the full layout
pipeline, which loaded the whole family into the font database for good
(faces are never unloaded). Every trip through the list kept tens of
megabytes. Entries are now drawn with FontDb::preview_face: a loaded
family's face is shared, an installed one is read into a face the
database doesn't keep and is freed when dropped, and render_text_line
draws it without the shared outline cache.

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

* Font preview test tolerates other tests loading fallbacks; gaps rows 1–3 de-duplicated, #848 noted in row 3

---------

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 11:14:56 -04:00
f7a84fbfb8 Print Markup: comments in PDF, print, images and text copies (#589) (#667)
Rebased onto main as one commit (the branch had merge commits from main).

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 23:38:29 -04:00
2544372a22 #611: DOCX diagrams: theme-styled shapes and connectors, VML lines (#671)
Shapes and connectors that take their fill and outline from the theme
(wps:style a:fillRef/a:lnRef) now show; scheme, preset, system, scRGB and
HSL colours resolve. Callouts and flowchart boxes draw as the nearest
shape. Legacy VML lines (with arrowheads), rectangles and ovals are read.
A DrawingML line runs top-left to bottom-right, as the preset does.

Rebuilt on main, which now has connectors, arrowheads and canvases (#416,
#344): those parts of the earlier branch were dropped in favour of main's.

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:15:28 -04: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
72e59f9f0b Drawing Canvas: insert a canvas and draw shapes, pictures and text boxes inside it (#344) (#356)
Insert > Illustrations > Shapes > New Drawing Canvas (and Draw > Drawing Canvas)
inserts a 6" x 3" canvas in line with text. While it is selected, Pictures,
Shapes and Text Box put the new object inside it. Fit, Expand and Scale Drawing
change its frame, Shape Fill and Shape Outline colour its background and border,
and canvas.member moves, resizes or removes a member.

The canvas is the group object with a canvas style: its frame is independent of
its members, and members outside it are clipped on screen and in PDF (new
Draw::Clip / Placed::Clip). DOCX reads and writes wpc:wpc with wpc:bg,
wpc:whole and wps:wsp / pic:pic members.

A canvas turns and flips as a whole, like a group (#332): its background,
outline and clipped members are drawn inside its turn (Draw::Rotated around
Draw::Clip; Placed::Clip carries the spin, its members sit in the unturned
frame), and hit testing follows the turned frame. The canvas schema has no
transform of its own, so a turned canvas keeps its rot/flipH/flipV as an
a:xfrm in a wpc:extLst extension (urn:wordcraft:canvas-xfrm), which Word skips.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:45:13 -04:00
8a2a8bb60d Insert > Icons: an original icon library, a searchable picker and Icon Color (#309) (#318)
Adds wordcraft_render::icon_lib, 61 monochrome icons in ten categories
(Arrows, Communication, Office, People, Nature, Weather, Shapes, Symbols,
Technology, Transport). They are original WordCraft artwork: simple
geometric line drawings designed on a 24x24 grid as path data in code, with
no icon files and nothing copied from another icon set.

insert.icon {"id", "color"?, "size"?} renders the chosen icon with vello_cpu
to a transparent PNG at about 300 ppi and inserts it at the caret through the
ordinary picture path, so it saves to .docx and every other format. Without
an id it lists the library (narrowed by query and category) and asks the
front end for the picker. picture.iconColor repaints a one-colour picture
in a new colour and keeps its transparency.

Insert > Illustrations > Icons opens a dialog with a search box, category
chips and a grid of icons drawn live by the egui painter from the same path
data; Insert or a double click places the selection. Picture Format > Adjust
gains an Icon Color menu. New strings are translated in every catalog.

Catalog parity 393/434 -> 394/434 (90.8%).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 13:27:42 -04:00
loganjandClaude Sonnet 5.5 144ddb7f2d Layout and render: extreme page numbers and wave borders no longer overflow or run away (#393)
- A page-number start near u32::MAX (.docx accepts up to 4294967295) or a line-number start
  near u32::MAX (open through .json and commands) overflowed when the next page or line was
  counted: a panic whenever overflow checks are on, silently wrapped numbers otherwise. The
  three additions now saturate.
- The wave stroke used by wave borders and underlines looped forever, pushing curves until
  memory ran out, once its x coordinate passed about 1e16 and `x + step == x` (a wave page
  border with a 1e20 `space`, reachable through .json, the control channel and MCP). The
  path is built in `wave_path`, which stops as soon as a step makes no progress.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 19:30:07 -04:00
e71e2d1c52 Draw tab: draw with pen, pencil and highlighter, erase ink, and hide ink (#315)
* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303)

Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser.
table.draw takes a pen stroke in page points: a rectangle outside a table
inserts a one-cell table of that size at the paragraph under its start; a
straight vertical or horizontal line inside a table splits the cells it
crosses (new grid column or row; untouched cells span it). table.eraser
merges the two cells beside the border nearest a point. Each stroke is one
undo step. In the app the buttons toggle pen and eraser modes with a live
preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks
don't move the caret while a tool is on. Splitting through part of a
vertically merged cell and erasing beside one are refused with a clear
error. Catalog 384/430 -> 387/430.

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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

* #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs

- draw.eraser with a point erases through draw.erase: test that it is one
  undo step (undo/redo) and marks the document dirty.
- Render and PDF: a single-point stroke's dot is drawn at the sanitized
  point, not the raw first one.
- DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are
  collected, across a:path elements.
- docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so
  Word shows it as a freeform.
- widgets test: theme::apply takes an Appearance (#317).

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

* fmt

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

* Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307)

Ink strokes are floating freeform shapes: a new Freeform geometry (paths in
shape-local coordinates, opacity, and the pen that drew it) on
InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one
anchored to the paragraph under the first point (one undo step; points
capped per stroke, strokes capped per page, NaN dropped); draw.erase /
draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil,
draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the
session view state; review.hideInk toggles showing ink on screen.

The canvas draws a live stroke while a pen is dragged and commits it on
release; the eraser deletes strokes it passes over (one undo step per drag);
Esc or Select goes back to selecting. Draw tab buttons show the tool in use,
with colour and thickness menus; Review gets an Ink group with Hide Ink.
Ink renders with round caps and joins on screen and in PDF; highlighters are
translucent and behind the text. DOCX writes freeforms as wps:wsp with
a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with
simple custom-geometry freeforms from other files.

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 18:50:37 -04:00
a7c809e63b Charts, SmartArt and OLE objects are no longer lost when you save a .docx (#333)
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Keep charts, SmartArt and OLE objects when saving DOCX (#319)

Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.

The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.

The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.

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

* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 17:48:51 -04:00
Valentin KleinandClaude Opus 5.5 bd10795306 Free rotation and flips for pictures, shapes and text boxes (#332) (#340)
Drawings carry an angle and horizontal/vertical flips on their placement
(Float::rot, flip_h, flip_v), read and written as DrawingML a:xfrm rot,
flipH and flipV for pictures, shapes, text boxes and groups; the written
effect extent covers the rotated bounds and reading takes that overhang
back out. Layout draws them turned about their centre (screen and PDF),
square wrap and inline lines keep clear of the rotated bounds, and hit
testing follows the turned shape. arrange.rotate now turns the frame
(90 degrees, flips) instead of rewriting pixels; arrange.rotation sets an
exact angle. The canvas frame has a rotation handle (Shift snaps to 15
degrees) and turned frames resize along their own axes; Picture Format
and Shape Format get a Rotation box and a Rotate menu. Text inside a
rotated text box stays upright for now.

Groups turn and flip as a whole (wpg:grpSpPr a:xfrm): members keep their
own turn inside the group's, are carried round its centre, and a group's
text boxes move with it. Shape effects are drawn inside the turn; a
shadow's direction follows DrawingML rotWithShape (kept on the model,
true when absent; Word's presets write 0, so their shadows keep their
page direction).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:10:09 -04:00