Commit Graph
386 Commits
Author SHA1 Message Date
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 66601038df tests(layout): build the nbsp test document without struct-update syntax (#4380) 2026-10-25 21:52:01 -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
Martin BallandClaude Opus 5.5 96579e6b4e #3692: repeated table header rows are capped by items, not rows
Audit finding 11: place_table cloned every item of the header rows onto
each page a table spills onto, but its cap (200 000) counted rows. An
exact-height header row keeps all its overflowing paragraphs without
growing, so one header row of thousands of items over hundreds of
pages placed millions of copies. The cap now counts placed items
(MAX_HEADER_ITEMS = 500 000, a row costing at least one) and the
header set repeats whole or not at all; past it later pages carry no
header rows and pagination still finishes. Test:
tests_budget::repeated_header_items_are_capped_across_the_table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Opus 5.5 8efba59b35 #3692: a linked text-box chain lays its story out at most 16 widths
Audit finding 7: flow() laid the head box's whole story out again at
every distinct box line length in the chain and kept each layout while
splitting, so a chain of many one-line boxes of different widths
multiplied the story's layout work and memory by the chain length.
Past MAX_FLOW_WIDTHS (16) distinct lengths the text stops: the box that
would need another layout and the rest of the chain show nothing, as
overflow past the last box is hidden. Work is now at most 16x the
story. Test: tests_budget::a_linked_chain_lays_its_story_out_at_a_bounded_number_of_widths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Opus 5.5 4963e30eed #3692: a row's gridAfter is bounded like the DOCX reader's
Audit finding 35: Table::cols() added a deserialized row's grid_after
(u32) unchecked, and layout_table sized its column vectors from it, so a
small native JSON document could ask for a 16 GB grid (and overflow the
sum on 32-bit wasm). Row::grid_after() now clamps to MAX_COLS as
first_grid() does and the importer does; cols() sums with saturating
adds; layout reads the bounded value. Test:
tests_budget::a_huge_grid_after_is_bounded_like_the_docx_readers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:25 -04:00
Martin BallandClaude Sonnet 5.5 1fa7383264 #3692: rebase onto main — drop the Ctx sections list the linked header/footer index replaces
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 00:19:17 +00:00
Martin BallandClaude Sonnet 5.5 17e648bde7 #3692: rebase onto main — sort the merged test module declarations (rustfmt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 00:19:17 +00:00
Martin BallandClaude Opus 5.5 ddb67eaa45 #3692: review — linear-time tests count lookup steps instead of timing them
The layout's linear-time tests (#3866) asserted CPU-time ratios, which a busy
CI machine can skew. Each walk that once ran again for every item it served
(a line's clusters, a page's items, the revision authors, the sections, a
table's rows, a run's glyphs, the clipped cells) now counts the elements it
looks at in a test-only counter (crate::work, nothing outside tests), and the
tests assert 8x the input takes at most 12x the steps (quadratic: ~64x).
proof_text, whose walk is the document model's, stays timed, and a ratio
over the limit is measured twice more before it fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-26 00:19:17 +00:00
Martin BallandClaude Opus 5.5 74dbec4107 #3692: findings 3 and 13 tests fail clearly on the old code
Finding 3: the table's rows carry no cells, only a column with a width skipped before and after,
so each row adds two spanning entries for next to no layout and counting the grid per entry
dominates (old code 75x for 8x the rows, fixed code 4-8x).

Finding 13: sections that link to the previous section's headers and footers are laid out
against the same sections naming them (title page on, so first-page parts are looked up too);
old code 6.1x the time, fixed code 1.0x, limit 2x.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:17 +00:00
Martin BallandClaude Opus 5.5 ee96c4c99d #3706: author colours ranked once per drawing, not per run or comment
Finding 30: author_index rebuilt the list of revision authors for every tracked run and paragraph mark drawn, and balloons rebuilt the comment-author list for every comment (quadratic in revisions or comments). display::author_order ranks authors by first appearance once; page_display, page_layers and object_display build the revision order lazily once per call and pass it down, and balloons builds the comment order once per layout. Same colours.

Tests: drawing_tracked_changes_by_many_authors_takes_linear_time, tracked_changes_colour_by_author_in_order_of_appearance, balloons_for_many_authors_lay_out_in_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 342855315e #3692: clipped cells looked up by path, not scanned per line
Finding 1: cell_clip searched every clipped cell on the page for each line drawn (quadratic in a page of many exact-height cells). clipped_cells now indexes them by story and content path, keeping each one's order; a line looks up only its path's prefixes and takes the earliest, the same cell the scan found first.

Test: drawing_many_clipped_cells_takes_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 5a07c46534 #3692: selections across a header index each page's pieces once
Finding 28: column_segments and selection_rects called pieces() per paragraph, and for a header, footer or note each call scanned the whole page (pieces_on). PartPages indexes a page's lines of the story by paragraph the first time the page is looked at; the page order (hint first, continued notes) is unchanged.

Test: selecting_a_long_header_takes_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 897add99a8 #3692: headers and footers linked through sections once per layout
Finding 13: linked() and defining_width() walked back through the sections before each section, per section and again per page (layout_pass, headers_footers, footer_table_wraps, header_table_areas). HfLinks resolves every section's linked header and footer sets incrementally once (each inherits the previous one's for the parts it leaves out) and keeps, per part id, the sections naming it, so the defining section is a binary search. Same sets and widths as before.

Test: headers_inherited_through_many_sections_lay_out_in_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 629711582f #3692: autofit counts the grid's columns once
Finding 3: column_content called Table::cols (a walk of every row) for each spanning cell, and there can be one in every row. Hoisted out of the loop; the end is also added with saturating_add.

Test: autofit_tables_with_a_spanning_cell_in_every_row_lay_out_in_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 4d027de402 #3692: math text runs find character boundaries in one walk
Finding 23: Ctx::glyphs found each character's first glyph by scanning the shaped glyphs from the start (quadratic in a long run). The first glyph at or past a boundary never moves back as the boundary grows, so one walk gives the same cuts.

Test: math::tests::a_long_text_run_lays_out_in_linear_time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 7d31c01f6f #3692: bidi clicks and caret steps walk a line's carets once
Finding 5: off_at_x and visual_step on a bidirectional line called x_of for every caret offset, each searching the clusters from the line's start (quadratic in a line of many zero-width offsets, e.g. hidden text). ParaLayout::line_carets walks them with a cursor; the first matching cluster never moves back for non-decreasing offsets, so the x's are the same.

Tests: bidi_clicks_and_caret_steps_take_linear_time, line_carets_match_x_of.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Martin BallandClaude Opus 5.5 6c20b3e609 #3692: proof_text walks the runs once
Finding 29: proof_text looked up each character's run with props_of_char, walking the runs from the first for every character (quadratic in a paragraph of many runs). A run cursor advancing with the text gives the same props in one pass.

Adds layout::tests_linear (sharing tests.rs's cost_ratio helpers) with proof_text_walks_the_runs_once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 00:19:16 +00:00
Dylan M. Taylor e5cffe7641 Merge pull request #4233 from VoiceLessQ/fix/small-cleanups
Small fixes: MERGEFIELD in any case, 18 SonarCloud closures, one unused constructor
2026-10-25 16:45:39 -04:00
skylar taylor-barrick 42477bd806 M0: reconcile current main and classify double-space rewrites for egui 0.37 2026-10-25 16:47:30 +01:00
skylar taylor-barrick f7c49cb0f8 egui-037: reconcile main and protect IME undo and wrapped layout 2026-10-25 16:15:05 +01:00
NetterSilverandClaude Haiku 5.5 4bf785c4cf Keep Arabic-Indic digits as typed in Arabic numeral mode (#3329)
Fixes #3259

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-25 10:44:31 -04:00
MarekSpodBiedry 14f0eec255 Stretch justified lines that end in a manual line break (#3924) (#4023) 2026-10-25 10:27:35 -04:00
skylar taylor-barrick 5b46f9ba4d M-egui037: fix fontless tests and document migration behavior 2026-10-25 15:24:23 +01:00
Marco Arnulfo 7563ebb314 Layout: a no-break space is as wide as a space, not as the font's own U+00A0 glyph (#4210)
* Layout: a no-break space is as wide as a space, not as the font's own U+00A0 glyph (fixes #4192)

* Layout parity: a no-break space is as wide as a space (#4192)
2026-10-25 10:23:25 -04:00
Marco Arnulfo 8c366eb176 Layout: a bookmark is no line-break opportunity (#4211)
* Layout: a bookmark is no line-break opportunity (fixes #4193)

* Layout parity: a bookmark is no line-break opportunity (#4193)
2026-10-25 10:19:38 -04:00
skylartaylor 5d428e408f M-egui037: reconcile upstream and document migration review fixes 2026-10-25 06:42:20 +01:00
aiongg 400bd131d3 Merge PR #4224: Fields: \* Upper, \* Lower, \* Caps and \* FirstCap change the result, and CardText is lowercase
# Conflicts:
#	crates/engine/src/cmd/references.rs
#	crates/engine/src/cmd/table_formula.rs
#	crates/layout/src/fields.rs
2026-10-25 04:54:09 +00:00
aiongg 8c035fbb2c Fields: \* ArabicDash formats any number field
It is a general format switch, so NUMPAGES, PAGEREF, SEQ and formulas
read it as PAGE does (#4226).
2026-10-25 03:51:09 +00:00
VoiceLessQ 65a0dc8dc6 Method paths for 18 closures that only call a method (SonarCloud S1612)
SonarCloud's analysis of main at b4da59f0c lists 23 closures such as `|v| v.as_slice()` that hand their argument straight to one method. This passes the method itself: `Vec::as_slice`, `str::to_ascii_lowercase`, `f64::total_cmp` and the like, all forms the repo already uses elsewhere. Behaviour is unchanged.

Five stay. The four in crates/proof/src/es/mod.rs sit in code that #4191 replaces, and its replacement already uses `PoisonError::into_inner`. In crates/engine/src/io/safe_save.rs the path would be `<dyn std::error::Error + Send + Sync>::is::<Guided>`, which reads worse than `|inner| inner.is::<Guided>()`.
2026-10-25 01:19:28 -02:00
dotCooCoo d9d61ee8da Fields: several capitalization switches apply in the order they appear
A field with \* Lower \* Caps applied only \* Lower, so a title of
WEEKLY REPORT showed as weekly report. Every capitalization switch in a
field code now applies in turn, and the example shows Weekly Report.
2026-10-24 19:39:35 -07:00
aiongg b8dfd9377a Merge PR #4226: Fields: \* DollarText and \* Hex write the result, and PAGE reads \* ArabicDash 2026-10-25 02:21:25 +00:00
aiongg 447254120a Merge PR #4224: Fields: \* Upper, \* Lower, \* Caps and \* FirstCap change the result, and CardText is lowercase
# Conflicts:
#	crates/engine/src/cmd/references.rs
2026-10-25 02:21:25 +00:00
dotCooCoo 0dd06b1048 Fields: \* DollarText and \* Hex write the result, and PAGE reads \* ArabicDash
A field or table formula with \* DollarText or \* Hex showed its digits,
and PAGE \* ArabicDash showed the plain page number. \* DollarText now
writes the result in lowercase cardinal text with "and" and the first two
decimals (rounded) over 100, so { = 9.20 + 5.35 \* DollarText \* Upper }
shows FOURTEEN AND 55/100. \* Hex writes uppercase hexadecimal digits
(458 shows 1CA). PAGE \* ArabicDash shows the page number between
hyphens ("- 31 -"); other fields, PAGEREF included, skip it. The
page-number, SEQ, PAGEREF and count fields and table formulas read both
switches. Cardinal, ordinal and dollar text keep the digits of a number
past 999,999. Before, \* CardText showed such a number as "nine hundred
ninety-nine thousand nine hundred ninety-nine" and \* OrdText as "nine
hundred ninety-nine thousand nine hundred ninety-ninth".
2026-10-24 17:57:38 -07:00
skylartaylor a605fa5ca7 M-EGUI037: reconcile WordCraft main and preserve IME editing 2026-10-25 02:19:38 +02:00
dotCooCoo c41251238b Fields: \* Upper, \* Lower, \* Caps and \* FirstCap change the result, and CardText is lowercase
Update Field and the page-number fields ignored the capitalization
switches, so TITLE \* Upper or FILENAME \* Lower showed the text as
stored. A \* CardText or \* OrdText result started with a capital
("Twelve"), where Word shows it in lowercase unless a capitalization
switch follows. A table formula with a \* switch, such as
=SUM(A1:B2) \* CardText or =SUM(ABOVE) \* MERGEFORMAT, was a syntax
error.

The results of the document information fields, DOCPROPERTY,
FILENAME, the dates, REF, PAGEREF, SEQ, STYLEREF and table formulas
now take the field's \* Upper, \* Lower, \* Caps or \* FirstCap switch
when Update Field computes them. PAGE, NUMPAGES, SECTION,
SECTIONPAGES, PAGEREF and STYLEREF take it when they are laid out.
Cardinal and ordinal text is lowercase ("twelve", "seventh") unless
one of the switches follows. WordCraft now reads a table formula's \*
switches separately from the formula. A whole-number result that a \*
number format such as CardText or roman covers is written in that
format; any other result, and any other \* switch, leaves the digits.
Table Layout › Data › Formula keeps the \* switches as typed.
2026-10-24 17:02:51 -07:00
aiongg 4a4877e433 Merge PR #4097: Layout: include exact line-bottom contact for floating tables (#3977) 2026-10-24 20:42:02 +00:00
aiongg 39bf1a6c76 Merge PR #4074: cu20we: fix box float paragraph origins and preserve shape line caps 2026-10-24 20:42:02 +00:00
aiongg e62af05110 Merge PR #4171: Picture memory limits for 6 audit issues: one decoder, TIFF strips, styles, Remove Background, Windows paste, render cache 2026-10-24 20:38:55 +00:00
aiongg 55fc2bed44 Merge PR #4090: Layout: a document of many pages paid a body-length scan per page 2026-10-24 20:38:44 +00:00
aiongg 3a205983e2 Merge PR #3836: Layout: a multiple-spaced line stays on the page while its text fits 2026-10-24 20:38:40 +00:00
VoiceLessQ d7429d217a Pictures: one decoder checks the header against the budget, progressive JPEG coefficients included (#3644)
Document pictures now decode through wordcraft_geom::picture (behind a
new `image` feature of the geom crate). It reads the header, refuses the
picture before anything is allocated when the decode's peak would pass
the budget, then decodes with the header's size and the budget as
limits.

The peak is the larger of three moments: the decoder's output beside
what it holds while decoding, the output beside its RGBA8 conversion,
and that beside a premultiplied copy. A progressive JPEG's decoder
(zune-jpeg) holds an i16 for every coefficient of every component, in
whole MCUs, for the whole picture; the SOF header gives that size. A
7900 x 7900 progressive JPEG without subsampling needs 9 bytes a pixel
(561 MB), past the 512 MB budget, although two RGBA buffers (499 MB)
fit. With the fix reverted, the test's header-only file of that size
was decoded and the test process peaked at 542 MiB resident.

- Render and PDF export ask the probe in their wrappers (decode_pixmap,
  load_image), so a picture past the budget gets the grey box or is
  left out, as before.
- The crop dialog and every picture edit decoded with load_from_memory
  and looked at the size afterwards. They decode through the probe now,
  and so do wrap outlines and the RTF re-encode.
- A picture edit has the drawing budget instead of an 80 M pixel cap
  checked after decoding. A picture past that budget (about 67 M RGBA
  pixels) can't be drawn either, so no edit that could be seen is lost.
2026-10-24 18:01:45 -01:00
nexotiqandGPT-6 7cf9ba6826 cu20we: merge current upstream and complete function contracts
Merge upstream main at 254351c01b,
retaining both roadmap histories and the existing anchor and cap fixes.
Document the changed DOCX reader, display builder, raster renderer and
PDF exporter contracts without changing their behavior.

Validation: cargo xtask ci passed all eight stages locally. Optional
craft-i18n catalogs and craft-fonts were absent. Independent local
coverage audit documents all 26 touched functions; CodeRabbit must
recalculate its own metric after a separately approved submission.

Co-Authored-By: GPT-6 <codex@openai.com>
2026-10-24 10:55:45 -05:00
OPUZZLES e39c00b3c1 #3977: use exact line height for table contact 2026-10-24 10:32:07 +02:00