* Type: story Optical Margin Alignment with a size, and Ignore Optical Margin (engine, IDML)
A story's Optical Margin Alignment applies to every paragraph except those
with Ignore Optical Margin; hang amounts scale with the story's size.
story.options sets it, type.para and paragraph styles take
ignoreOpticalMargin. IDML imports and exports StoryPreference
OpticalMarginAlignment/OpticalMarginSize and IgnoreEdgeAlignment. The
per-paragraph opticalMargin switch keeps hanging at the text's own size.
Part of #947.
* Type: the Story panel, and Ignore Optical Margin in the Paragraph panel menu and Paragraph Style Options
Window > Type & Tables > Story sets Optical Margin Alignment and its size
for the stories of the text selection or the selected text frames through
story.options, and is disabled without either. type.selectionAttrs
reports the stories' values as storyOptions, null where they differ.
The Paragraph panel menu toggles ignoreOpticalMargin, and Paragraph Style
Options > Indents and Spacing has an Ignore Optical Margin checkbox.
Part of #947.
* ROADMAP: the story Optical Margin Alignment entry names its default and scope
* Anchored text frames compose against the grid and page where they are placed
An anchored text frame is not a spread item, so frame_specs composed it at
its stored transform: no page, no page side, no page rect, and a baseline
grid phase taken from that transform. A note set to Only Align First Line
to Grid snapped to the wrong line, and page-number markers and
spine-relative alignment inside it saw no page.
The composer now looks up the host story's composition first, finds where
it placed the frame (in its frames' anchored objects, footnotes or table
cells) and composes the frame's story against that placement
(ComposeOptions::placed). The cache does the same and adds the placement
to the entry's signature, so a note recomposes when its host moves it.
Host lookups stop after eight levels, which bounds stories anchored in
each other.
* Anchored text frames: the cache keys on the placed spread's pages
Two unbounded things in the GREP-style path.
`BACKTRACK_LIMIT` bounds one search, not a scan: `find_all` was called with
`usize::MAX` and pays the limit again for every match, so a pattern tuned just
under it cost `200_000 x matches` per paragraph on every relayout. A scan now
stops at `MAX_MATCHES_PER_PARAGRAPH` (4096) -- more styled runs than a
paragraph can usefully carry -- and says which pattern reached it.
The `PATTERNS` thread-local never evicted, so it grew across every pattern a
session ever composed. It now holds at most 64, least recently used dropped
first: a tick counter marks each use, so the entry evicted is the one longest
unused rather than the one inserted earliest. The test checks that the pattern
just used is still there after the cache overflows, which insertion-order
eviction would not give.
Left for a separate change: DC-17 also asks for a per-compose step budget, so
the cost cannot add up across paragraphs. That means threading a budget from
`compose_story` through `shape` into `overlays`, a compose-internal signature
change that wants its own review rather than riding along here.
The new command note.convertFromText removes the selected story text
and puts it into a new editorial note whose anchor sits where the text
was. It takes an optional author, and Convert Note to Text puts the text
back. DesignCraft refuses a selection in a table cell, or one that holds
a note, footnote, endnote, anchored object, index marker, text anchor,
cross-reference or table, and leaves the story unchanged. The item
follows New Note in Type › Notes, with translations in the catalogs that
already translate Convert Note to Text.
The importers each had their own spelling of "is this a number we can use", or
only checked that it was finite. Measured before the change:
<cSize:1e300> -> Some(1e300) pt tagged text
<cSize:-5> -> Some(-5.0) pt
<cSize:0> -> Some(0.0) pt
w:spacing before=1e300 -> 5e298 pt indent DOCX
DC-12 asks for a shared clamp helper used by every importer, so there are two
in `doc::attrs`, beside the tab limits:
* `imported_type_size` drops a size the engine cannot mean, so the text keeps
the size its style gives it -- which is what RTF and DOCX already did.
* `imported_measure` clamps an indent, spacing or offset to the widest page.
Clamped rather than dropped on purpose: a file that says "indent everything"
still indents as far as the engine goes, where dropping would quietly restore
the style's own indent and hide what the file asked for.
Both are now used by tagged text, DOCX runs and paragraphs, DOCX numbering
levels and RTF -- the last two had the 0.1-1296 pt range as a literal, which is
the duplication that let tagged text drift.
Control: with the helpers back to a finite-only test, both new tests fail, the
DOCX one printing the 5e298 pt spacing in full.
Stale in the issue: its first location, `docx.rs` `w:sz`, is already clamped --
that came with #1574. DC-12's remaining holes are the ones fixed here.
Update, Relink and Relink Folder all reload a linked graphic through `reload`,
which read the file with an unbounded `read`. A document whose link points at a
multi-GB file exhausted memory on Update. How large a linked file is belongs to
whoever wrote the document, not to whoever opens it: the path comes out of the
document.
`reload` now reads through `read_capped` at the limit packaging already applies
to a linked file (`MAX_LINKED_FILE_BYTES`, 256 MB), and a file over it is
refused by name instead of read. `reload_capped` takes the cap so a test can
show both sides without a 256 MB fixture.
Not changed: `file.place` reads a path the same unbounded way, but there the
user picked the file, where a link's path can come from a document someone else
wrote. Whether that read wants the same cap is a call for a maintainer, so it
is reported rather than changed.
Found in the audit of #1596 (DC-15).
Found while testing crates/compose/src/notes.rs:133 (also crates/doc/src/notes.rs:265 footnote_start and crates/doc/src/endnotes.rs:150 endnote_start)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Angle field was greyed out with Use Global Light on. It now shows
the document's global light angle and changes it for every effect that
uses it, applied with the effects in one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
package_refuses_windows_directory_redirects made its redirect with
symlink_dir. Without Developer Mode or elevation that call fails with
os error 1314 (ERROR_PRIVILEGE_NOT_HELD), which std does not report as
PermissionDenied, so the skip arm did not match and the test panicked.
When symlink_dir fails, the test now makes a directory junction with
mklink /J, which needs no privilege. is_redirect treats a junction as
a redirect, so the test still checks that File > Package refuses the
redirected folder. It skips only when neither can be made. The package
code is unchanged.
a_missing_link_shown_by_its_preview_is_still_reported stored the link
/definitely/missing dir/photo.jpg in a document without a path. On
Windows that path names no drive, so document_path refuses it as not
absolute and the preflight message ends with "(the path is not
valid)", and the test failed. The test now stores a link under
std::env::temp_dir(), an absolute path with the default
temporary-folder settings on Windows, macOS and Linux, and expects the
message for that path. The preflight code is unchanged.
* chore: resolve SonarCloud maintainability issues
- Replace lambda closures with method references (S1612)
- Replace wildcard imports with explicit imports (S2208)
- Refactor complex functions with extracted helpers (S3776)
- Replace OR pattern with range pattern in math parser (S9047)
* style: apply rustfmt to warichu helper
* fix(compose): revert slice conversion in tests and remove unused var in warichu
* Group warichu row layout scalars to satisfy S107 parameter limit
* style: satisfy rustfmt in warichu.rs
---------
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Preserve existing PDF catalog entries and full object generations. Accept literal null trailer entries while failing closed on encrypted/hybrid/ambiguous or unreadable history. Validate visibility variants atomically before document edits, suppress stale unsupported canvas overrides, and preserve conservative content cropping without overcropping empty optional-content stubs.
Local full CI: all 9 gates passed, 3063 tests passed and 6 skipped, all 16 WASM-safe crates checked. Independent qpdf/preservation/Poppler checks passed for17 supported inputs;6 refusal cases stayed refused. All11 additional Prev controls passed. GitHub CI and CodeRabbit passed on00d5fa5c; CodeRabbit confirmed the Prev finding addressed. Author history preserved; no native format changes.
Reject a non-null Prev entry that cannot be read as an offset instead of silently treating it as the end of the guarded trailer chain. Keep absent/null end markers and readable numeric offsets unchanged.
Reproduced CodeRabbit discussion_r4366245854 with an independent probe and a failing native regression before the fix. The synthetic regression covers seven unreadable value types plus absent/null and integer/real history controls.
Validation: all 9 cargo xtask ci gates passed (3063 tests passed, 6 skipped; 16 WASM-safe crates). One unrelated GPU test was labelled leaky by nextest in that run and passed cleanly in an isolated rerun. Independent checks: 17 original supported PDF variants still pass qpdf, logical preservation and Poppler pixels; 6 original refusal cases remain refused. All 11 additional Prev controls pass (7 refused, 4 accepted).
Integrate current main (d8d78851) without rewriting the contributor's commits, retaining craft-libs 9f75d3dc.
Accept literal null trailer entries while rejecting actual or ambiguous encryption/hybrid markers. Structurally inspect bounded xref/trailer history instead of adopting trailer-looking comments. Preflight all selected source variants before editing/undo; preserve reset-to-defaults. Suppress stale unrewritable canvas overrides rather than revealing source content. Keep conservative all-content crop fallback without overcropping layerless/null stubs.
Validation: cargo xtask ci, all 9 gates passed; 3062 tests passed, 6 skipped; all 16 WASM-safe crates checked. 17 supported variants passed independent qpdf, logical preservation and Poppler pixel checks; 6 unsupported/repair-only cases refused. Direct-harness Claude Opus 5 reviewed the exact main-relative delta with no blocking findings.
No native-format changes or unrelated layer features. Synthetic regression fixtures are generated in test code.
Found while testing crates/engine/src/cmd/table.rs:1742
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>