Commit Graph
427 Commits
Author SHA1 Message Date
Claude 6f6559aacb ODT: widow control as a profile case; tabs measured from the indent kept
Widow and orphan control a file doesn't state is open in the standard
(it refers to XSL, whose initial value is 2); LibreOffice reads it as
off. It is now a profile case (widowControl), and WordCraft writes it on
its default style either way.

LibreOffice measures tab stops from the paragraph's indent when its
TabsRelativeToIndent setting is on (its default), default stops too
(measured). WordCraft's layout now counts default stops from the indent
for such documents, and writes them back measuring from the indent
instead of from the margin, which moved LibreOffice's default stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 06:18:46 +00:00
Claude e635c8bd96 M5 #19: ODT cell background pictures
LibreOffice's elegant agenda puts a small gradient picture behind cells
(§17.3 style:background-image in a table-cell style). Cells can now
carry a background picture (CellProps::picture: media, own size,
repeat none/tile/stretch, alignment), which layout draws over the
cell's shading and under its text, clipped to the cell (Placed::Clip of
Placed::Image; tagged as an artifact in PDF). The ODT reader reads it
(style:repeat, style:position, the picture from the package) and the
writer writes it back with the picture in the package (once per
picture).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-03 00:10:57 +00:00
Claude 5063000781 M5 #19: ODT keeps table styles, SmartArt and section column balance
Found by saving WordCraft's own documents as ODT and opening them in
LibreOffice (42 documents: templates, themes, one per feature).

- Table styles: ODF has no table styles of Word's kind, so each cell now
  states what the style gives it (fills, its borders, bold header text,
  its paragraph spacing), under the paragraph's own styles as layout
  applies them (ECMA-376 §17.7.2; StyleSheet::table_para_patch and
  table_char_patch). The style's border mapping moved to
  styles::part_borders, shared with layout.
- Tables read from ODT no longer get Word's Table Grid style, whose
  single spacing changed their text.
- Columned sections say whether their columns end balanced
  (text:dont-balance-text-columns, §20.428, read as its name says; its
  value descriptions contradict it). Unstated, the ODF profile decides
  (balanceSectionColumns: LibreOffice balances, WordCraft as Word).
  SectionProps::balance_columns carries it to layout.
- SmartArt diagrams are saved as a draw:g of their shapes and text (with
  an invisible frame keeping the graphic's box), which LibreOffice draws
  as WordCraft does.

LibreOffice round-trip corpus: 40k differing pixels over 125 documents
(was 76k), 42 identical; WordCraft-origin documents 277k -> 145k.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 23:24:24 +00:00
Claude 8d5485c8bd M5 #19: ODF interpretation profile (WordCraft / LibreOffice / custom)
What ODF 1.3 defines, including XSL's initial values for fo: properties,
is followed whatever the settings: unstated page margins are now 0, and
padding without a border takes room in every ODT document. What the
standard leaves to the application (Part 3 §16.2) is an OdfProfile:
WordCraft's own choices by default, LibreOffice's, or custom values.
The profile fills in only what a file leaves unsaid; stated values win
under every preset (tests_more/profile.rs).

Cases: header height, how its height and spacing combine, dynamic
spacing, tabs from the indent, column gap, shape fill, line and
hairline, page frames against a paragraph, in-line frames against a
paragraph or page, and lines of only pictures.

- doc: odf_profile module; Settings::odf (OdfLayout) replaces odf_layout
- formats: import_with / import_flat_with take a profile
- engine: Prefs::odf, file.options `odf`, opened files follow it
- ui: File > Options > Advanced > OpenDocument files (presets, custom cases)
- cli: WORDCRAFT_ODF=wordcraft|libreoffice
- docs: odf-interpretation.md lists every case (law, open, writer
  choices, Google Docs and ONLYOFFICE findings); parity docs, gaps,
  ROADMAP updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 20:54:05 +00:00
Claude be2666da15 Gap 19: ODT padding without a border, unequal columns, column breaks
Found with LibreOffice's report templates and one-page résumé.

Padding without a border:
- LibreOffice gives a paragraph's padding room on every side, with or
  without a border; measured, 0.5 in of padding and no border moves the
  text 0.5 in in and down. WordCraft dropped it.
- The reader now keeps it (an invisible border holding the space; a new
  `Borders::takes_room()`), and the writer writes it back.
- The layout adds it to the spacing or indent under the document
  setting, now renamed `Settings::odf_layout` to cover all of
  LibreOffice's layout rules. `ResolvedPara::padding` carries it, so
  Word documents are unchanged.

Columns:
- Unequal columns (`style:column` relative widths and indents, on pages
  and sections) were made equal. They now keep their widths and are
  written back as shares. The writer leaves out `fo:column-gap` beside
  them, because LibreOffice ignores a page's column widths when a gap is
  stated (checked).
- Columns with no stated gap have none, where they had Word's 0.5 in.
- A paragraph starting a column (`fo:break-before="column"`) now starts
  with Word's column break, written back on the paragraph.

The report templates round-trip to 0-11 differing pixels (from up to
9,600), and the résumé to 24 (from 6,286) with its text the same.
Across the corpus the round-trip difference falls from 155k to 82k
pixels. WordCraft's own rendering difference falls from 469k to 428k,
and no document gets worse.

gaps.md row 19 and the ROADMAP log are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 15:39:32 +00:00
Claude 959770fa13 Gap 19: ODT lines of only pictures are as tall as the pictures
LibreOffice sizes a line holding only in-line pictures by the pictures
alone. Its font's ascent and descent don't count: measured in
LibreOffice, a 3.6 pt rule's line is 3.6 pt, where WordCraft (following
Word) made it 15.5 pt.

- A new document setting, `Settings::object_lines_fit_objects`, makes
  the layout size such lines by the pictures alone. The ODT reader turns
  it on, so other documents keep Word's layout. An equation or any text
  on the line keeps the font's height.
- A document with no page layout at all (no styles.xml) opened on Word's
  Letter page with 1 inch margins. It now uses the region's paper with
  LibreOffice's 2 cm margins.

WordCraft's own rendering of LibreOffice's elegant agenda comes close to
LibreOffice's: its pixel difference halves, from 17,428 to about 8,950.
Across the corpus WordCraft's rendering difference falls from 487k to
469k. No document gets worse, and round trips are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 14:51:51 +00:00
Claude 8f65b867c0 Gap 19: ODT tables fit the margins and keep their spacing; header spacing kept
Found with LibreOffice's report templates.

Tables:
- A table filling the margins (`table:align="margins"`) but wider than
  them ran off the page. It is now fitted to the text width, its columns
  in proportion. Checked in LibreOffice, which fits such a table with or
  without relative column widths; the old test's claim otherwise is
  corrected.
- A table's own space above and below (`fo:margin-top` and
  `fo:margin-bottom` on the table) was dropped. It is now read into new
  `TableProps::space_before`/`space_after`, laid out (added to the
  spacing of the paragraphs around it, as LibreOffice does) and written
  back.

Headers and footers:
- A header taller than its band lost its spacing from the body, moving
  the text up. LibreOffice keeps the spacing below the content unless it
  is dynamic (`style:dynamic-spacing`, which behaves as Word does). New
  `SectionProps::header_gap`/`footer_gap` hold it. The layout keeps it
  past a tall header or footer, and the writer writes it back. Word keeps
  no gap, so DOCX documents are unchanged.

Across the 125-document corpus, the round-trip pixel difference falls
from 241k to 185k, and WordCraft's own rendering's difference from
LibreOffice's falls from 562k to 484k. No document changes page count or
gets worse; one report template falls from 7,646 to 602 pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 13:38:36 +00:00
Claude 5cdf2608bf Gap 19: ODT tables fixed layout and starting a page; a page break before a table's first row moves it
- A table starting a page (`fo:break-before="page"` on the table,
  LibreOffice's agenda minutes) didn't. Its first paragraph now has a
  page break before it, and it is written back on the table.
- Layout: a page break before the paragraph in a table's first row
  now moves the whole table to the next page, as Word does. It was
  ignored; this matters for Word documents too.
- ODT tables are now fixed layout (Word's `w:tblLayout`).
  LibreOffice's columns never fit their content.

LibreOffice's 6orange agenda now round-trips with the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 10:08:59 +00:00
Claude 078945e10c Emphasis marks: modelled, drawn, set, and kept in DOCX and ODT
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
  to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
  is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
  it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
  `w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
  disc the dot, a dot below the under-dot.

Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
  already draws nothing for SET, so its result stays as it was, as
  `ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 09:46:23 +00:00
Claude b87c80cdc7 Layout: text in a turned text box turns with it
A turned text box (free rotation) drew its frame turned and its text
upright. Its text now turns with it about the box's centre, as Word
draws it, on screen and in PNG and PDF export: upside down when the
box is flipped across, never mirrored by a side-to-side flip.

PDF export draws item by item, to tag each paragraph, so it turns each
of the box's lines the same way, and tags the turned lines as their
paragraph's text rather than as decoration.

Clicks and the caret inside a turned box still go by its upright text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:42:07 +00:00
Claude 45b852ad4a Layout: in-line drawings move with their run's raise or lowering
An in-line picture, shape or chart always stood on the baseline,
whatever its run's position. Word moves an in-line picture with its
run's position (`w:position`, Font › Advanced › Position):
- lowered, it reaches below the baseline (at most until its top is on
  the baseline) and the line's descent grows to hold it;
- raised, it stands clear of the baseline at its own height.

A drawing's cluster now reaches `obj_h` above the baseline and `obj_d`
below it (negative when raised). It is painted, hit-tested and wrapped
from both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 17:24:15 +00:00
Claude f217aafa53 Layout: a sloped line's arrowheads sit at its ends
The renderers draw a line from its frame's top left to its bottom right
(DrawingML's `line`; flips turn the frame), but the arrowhead code and
`end_direction` assumed a line from the bottom left to the top right, so
a sloped line's arrowheads were drawn at the other diagonal's corners,
apart from the line. Horizontal and vertical lines hid it. The
arrowheads now start from the line's own ends, pointing along it.

Regression test: `tests::a_lines_arrowheads_sit_at_its_ends`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 07:15:27 +00:00
Claude e15a86bd55 Gap 19: ODT text box frames keep their line, fill, padding and fit
A text box frame's line (colour and width), fill and padding are read
from its graphic style and that style's parents (LibreOffice's Frame
style included) instead of a fixed thin black line on white, and written
back. ODF's defaults (no line, fill or padding) apply where the styles
say nothing. A frame without a height, or with a `fo:min-height` on its
text box, opens as a box that resizes to fit its text and is saved with
a least height again. A LibreOffice text-box document was 1,117 px apart
after a round trip; now 736.

Layout: a floating text box that resizes to fit its text now takes its
text's height (drawn and wrapped around), as inline ones already did;
before, it kept its stored height (an ODT frame without one showed as a
1 pt line with the text spilling over the paragraph).

A formula sub-document's content.xml stays a bare `math:math` root, as
ODF 1.3 Part 3 §2.2.9 B requires (pinned by a test), and the reader now
skips a wrapping `office:math` some writers add instead of taking it for
the MathML root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 03:34:12 +00:00
a4bcf05e11 Layout: a kept heading goes over with a three-line paragraph that widow control moves whole (#4991)
Keep with next asked for two lines of the paragraph after a kept heading under widow/orphan
control. A three-line paragraph cannot split at all there (two lines and a widow, or an orphan
and two), so it moved over whole and left the heading alone at the foot of the page. Word moves
the heading with it: ask for all three lines in that case.

Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-31 10:49:46 -04:00
ReesandBrandon Thomas a4b1682b41 Index laid-out objects during connector rerouting (#3631) (#4622)
* #3631: index laid-out objects during connector rerouting

* docs: keep the gaps.md note in the status line and revision table

* docs: drop a stray blank line

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 00:13:32 -04:00
2f78615291 #4720: Chart and SmartArt text the font lacks is drawn in a fallback face, so Chinese labels no longer show boxes (#4726)
Chart and diagram text was shaped in one face, the document's body font,
so Chinese legends and axis labels showed boxes while body text was fine.
Characters the face lacks now go to a fallback face, one face per run
when one covers it all, as in body text; a bold fallback face without a
bold style is emboldened.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-30 00:12:55 -04:00
Fin WarmanandBrandon Thomas f658c872a2 Tables: share spanning minima by preferred widths (#1883)
* Tables: share span minima by preferred widths (#1883)

* 1222: Match nested preferred span weights

* docs: word the #1883 layout-parity entries

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 21:40:41 -04:00
Dylan M. Taylor 937c7e532d Refactor layout for strict Clippy checks 2026-10-28 01:06:48 +00:00
aiongg 4b8b06fe03 Merge pull request #4602 from Silvertree2010/perf/export-speed
Speed up PDF export by about 24 %
2026-10-27 13:41:24 -04:00
aiongg f8e327b068 Merge pull request #3936 from benandez/fix/3907-section-marks
#3907: continuous sections running off a page go onto the next; empty section marks in a row stack to the page bottom
2026-10-27 12:37:46 -04:00
NetterSilverandClaude Opus 5.5 ffa127c615 Skip per-character table lookups for plain text in layout and PDF export
Shortcuts that give the same answers as the lookups they skip:

- is_rtl: nothing before U+0590 is right to left, so Latin, Greek and
  Cyrillic text skips the bidi class search.
- strong_rtl and is_mark: ASCII letters are the only strong ASCII
  characters, and no ASCII character is a mark.
- cluster_starts: in ASCII text every character starts a grapheme except LF
  after CR, and no ASCII character joins a cluster.
- display lines: a glyph run's vectors and text are sized up front, and the
  symbol-font check is made once per run instead of once per cluster.
- PDF glyph runs: the baseline (the y most glyphs share) is only worked
  out for italic runs, the only ones that use it, and a run on one baseline
  returns it without counting.

Tests check each shortcut against the full lookup (every code point for the
bidi classes, every ASCII pair for the clusters).

PDF export of the export_bench document, interleaved with main: median
293 ms -> 241 ms (-18 % with the previous commit). The PDF bytes are
identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:39:09 +01:00
NetterSilverandClaude Opus 5.5 8cdc172171 Lay out the paragraph after a kept heading once
A paragraph with keep with next measures the paragraph after it before it is
placed (next_first_line). That measurement called layout_para directly, so
every paragraph following a heading was laid out twice in each layout pass:
once to measure it and once, through the cache, when it was placed.

The measurement now goes through the layout cache under the key
para_labelled uses (Ctx::para_ahead) when its inputs are the same: no
proofing, no typed word, not inside a text box, no inline text boxes that
resize, the body's tracked-deletion view. Otherwise it is laid out on its
own as before. It counts nothing (list labels, equation numbers and the
page-total flag are restored), so pagination is unchanged.

Adds crates/engine/examples/export_bench.rs, which saves a long generated
document (the sample document 200 times, 259 pages) through io::save_bytes
and prints wall times:

  cargo run --release -p wordcraft-engine --example export_bench -- pdf 200 9

PDF export on that document, interleaved with main (Ryzen, 16 threads):
median 286 ms -> 256 ms (-10.6 %). The PDF bytes are identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:39:09 +01:00
Martin BallandClaude Opus 5.5 1b485e1f90 #3907: CodeRabbit — a kept paragraph is measured against the next page's body (a different first-page header)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:24:50 +00:00
Martin BallandClaude Opus 5.5 751eb4b204 #3907: CodeRabbit — a stacked mark past the page bottom draws no line number either
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:39:26 +00:00
Martin BallandClaude Opus 5.5 5225569d44 #3907: review — an empty mark taking no room, or stacked past the page bottom, draws no list label; an empty mark after a text paragraph closing a section stays quiet
dpkruse's Genuine Steps Certificate: a bulleted text paragraph closes a
two-column continuous section, then an empty bulleted one closes another.
Word draws no bullet for the empty one; it is now quiet (takes no room) and
draws no label, still counted. A stacked mark that doesn't fit keeps its
line for the caret but draws nothing below the margin (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:04:05 +00:00
Martin BallandClaude Opus 5.5 f2fe7654e9 #3907: review — a kept paragraph low in a continuous section moves to the next page; a page break before a stacked mark keeps the new page's bottom
CodeRabbit (local): keep-lines compared its height with the room below the
mid-page section start, so a paragraph that fits on a new page split instead.
It now compares with the next column, or the next page's body when this is the
last column. A stacked mark whose page break before starts a page no longer
gets the previous page's bottom back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:56:44 +00:00
Martin BallandClaude Opus 5.5 7757828295 #3907: continuous sections running off a page go onto the next; empty marks in a row stack to the page bottom
A continuous section moves the column top down below the text above. at_top()
then held at every new section start, so a paragraph that did not fit was
placed anyway, as on an empty page. A run of one-line continuous sections ran
off the bottom of page 1 (100 of them reached 2,480 pt down a 792 pt page).
The overflow checks for paragraphs now use fresh(): at the top with nothing
above on this page. at_top() still drives the spacing rules, unchanged.

Empty paragraphs that close a continuous section right after another
section's mark took no room. Measured in Word on synthetic files ("Start", N
such marks, "End"): 10 put "End" about 9 cm down page 1; 50, 100 and 200 all
put it on the first line of page 2, the same with 24 pt marks or no paragraph
spacing. So each takes its line while it fits, then stays at the page's
bottom. A mark right after text stays quiet, as measured before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:56:35 +00:00
Martin BallandClaude Opus 5.5 04ebc4ed42 #3995: Thai one-face fallback tests skip unless a Thai font with Latin is installed
Noto Sans Thai and its siblings have no Latin letters or digits, so on a
machine whose only Thai fonts are Noto no face covers the test run and
fallback_for_run rightly returns None. The guard now asks whether the Thai
substitute covers the whole run, which doesn't go through fallback_for_run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-27 09:52:32 +00:00
aiongg 5113be5569 refactor(layout): split math.rs into math/ 2026-10-26 21:16:49 +00:00
aiongg 5a84dd2f4f refactor(layout): split display.rs into display/ 2026-10-26 21:16:49 +00:00
aiongg 4f74183576 refactor(layout): split para.rs into para/ 2026-10-26 21:16:49 +00:00
aiongg 15f50c96df refactor(layout): split lib.rs into modules 2026-10-26 21:16:49 +00:00
aiongg fe73d141fe refactor(layout): move tests_*.rs files to tests/ 2026-10-26 20:54:02 +00:00
aiongg 3b84c637ea refactor(layout): move math tests to math/tests.rs 2026-10-26 20:54:02 +00:00
Dylan M. Taylor d859c673c1 Sonar: reduce cognitive complexity in highlight, review and outline commands 2026-10-26 16:01:36 -04:00
Martin BallandClaude Opus 5.5 49c2f61373 #3866: tests_linear imports Page and Rgb (main's layout tests didn't compile after #4392)
#4392's tests used Page and Rgb, which other changes merged around it no longer
brought into scope, so main's CI gate fails with E0425/E0422 in tests_linear.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2
2026-10-26 08:08:46 +00:00
Dylan M. Taylor 2c99a42780 Merge pull request #4392 from benandez/fix/3866-followup
#3866 follow-up: PDF export draws items with authors and cell clips built once per page; endnote selection linear
2026-10-26 01:31:58 -04:00
Martin BallandClaude Opus 5.5 b94821ea16 #3866: review — count a step per page walked in the first-page index, so the hinted-page test sees an eager build
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 04:05:47 +00:00
Martin BallandClaude Opus 5.5 e71c9ce31d #3866: review — the first-page index is built only when the hinted page doesn't show the paragraph
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 04:01:47 +00:00
Martin BallandClaude Opus 5.5 ecdc90cdc4 #3866: review — port the clipped-cell lookup test (nesting, first in page order, other story/row) to the layout crate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 03:15:35 +00:00
Martin BallandClaude Opus 5.5 e9dd252096 #3866: review — PDF export builds the revision authors and clipped cells once per page; part_pieces finds a paragraph's first page from one index; grid_cols once; balloon colour test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 02:57:08 +00:00
Dylan M. Taylor a95a570edc Merge branch 'main' into sonar/newcode-lints-1 2026-10-25 22:47:05 -04:00
Dylan M. Taylor 66601038df tests(layout): build the nbsp test document without struct-update syntax (#4380) 2026-10-25 21:52:01 -04:00
Dylan M. Taylor 4bb3679e41 Sonar: use PoisonError::into_inner, explicit test imports, [[ in import-cert
- proof: pass PoisonError::into_inner instead of an identity closure (S1612).
- layout tests: replace glob super::* imports with explicit names (S2208).
- packaging/macos/import-cert.sh: use [[ for the status test (S7688).
2026-10-25 21:28:59 -04:00
Martin BallandClaude Sonnet 5.5 3ee1978910 #3692: rebase onto main — rustfmt after the merge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 3cffe22c00 #3692: review — picture outline cache keys stay unique: the per-pass one documented, the lasting one holds its bytes' allocation
Alphas keys decoded pictures by their bytes' address and length, which is
sound only while the document is borrowed for the layout pass; that is now
said where the key is. LayoutCache's outlines outlive the documents they were
made for and use the same key, so each entry now keeps a Weak reference to
the bytes: their allocation can't be freed and the address reused by another
picture of the same length while the entry is kept. MAX_FLOW_WIDTHS spells
out which layout a box past the 16-width cap reuses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 1ae38ec2be #3692: review — stacked linked boxes share one layout; past the cap a box takes one in its own direction
Stacked text is laid out at STACK_WIDTH whatever the frame, so its flow key now is too.
The nearest-layout fallback only picks layouts of the box's TextVert; the first box in a
direction is laid out (at most a few more layouts).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 0f9b926497 #3692: review — a picture's transparency is kept for the whole layout, not just the last one decoded
Crops alternating between pictures decoded each again and again, spending the budget on
pictures that fit in it. Every picture charged to the budget keeps its alpha (bounded by
the same 2^28-pixel budget).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 32e4900bc5 #3692: a linked chain past 16 widths reuses the nearest; opaque pictures and budget refusals don't cost a later picture its outline
Ultrareview finding 6: boxes past a chain's 16th distinct width (compared
by exact float equality) showed no text, which resized boxes hit easily.
Widths are now told apart to a twip, and past the cap a box takes the
nearest layout already made (same direction, widest that fits, else the
narrowest). Work stays bounded at 16 story layouts.

Ultrareview finding 7 (also raised by CodeRabbit): opaque non-JPEG
pictures were decoded and charged to the per-layout pixel budget though
they give no outline, and a budget refusal was cached in LayoutCache as a
permanent "no outline". The header (read via into_decoder, PNG tRNS
included) now says whether a picture can be transparent before anything is
charged; Alphas::outline reports OverBudget distinctly from Ok(None), and
Ctx::wrap_outline (via picture_outline) doesn't keep a refusal in
LayoutCache::outlines. Within one layout a refused picture is remembered
so its header isn't re-read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:51 -04:00
Martin BallandClaude Opus 5.5 b578be0459 Audit finding 38: the outline cache keys on media and crop, but working
out any crop's outline decoded the whole shared picture again, so many
small picture references with distinct crops multiplied full-image
decodes without repeating the encoded image. Layout now keeps the last
picture's transparency (one alpha byte per pixel) and reuses it for
every crop of it, and charges each decode's pixels against
MAX_DECODED_PIXELS (2^28) per layout; past it pictures wrap as their
rectangles. Tests: tests_budget::a_picture_cropped_many_ways_is_decoded_once,
tests_budget::decoding_pictures_stops_at_the_layouts_budget.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:44 -04:00