Commit Graph
2570 Commits
Author SHA1 Message Date
skylar taylor-barrick 08876e1868 M6: refuse an export only for a choice it could have applied
The refusal guard asked whether any item still had the layer fields, while the export walks
also require a graphic whose asset is a PDF. Relinking a placed PDF to an image keeps those
fields on the item (links.relink replaces the asset in place), so a stale field blocked IDML
and PDF/A-2b exports that used to work. The guard, and the package report, now ask the same
question the walks ask: has_applicable_pdf_layer_overrides requires the PDF asset too, so a
stale choice is simply ignored and the tripwire still fires if a future construct ever puts a
real choice where the walk cannot reach it.

Regression: a_stale_choice_on_a_relinked_image_does_not_block_exports places a layered PDF,
hides a layer, relinks that asset to a PNG and exports IDML, PDF/A-2b and plain PDF - all
succeed, and the document reports the stale field while the applicable helper does not.
2026-10-24 16:11:28 -06:00
skylar taylor-barrick 1266ce8236 M6: cover every placement when PDF layer choices are applied
Kept separate from the parent-page fix and from the all-on export fix: this is the traversal
remediation on its own.

The detection helpers already walked parent spreads, groups and the stories in table cells,
notes and endnotes, while the two exports walked document spreads only, and the write side
(Document::update_items) did not reach placements nested in those stories either. So a choice
on a frame anchored in a cell's text or in a footnote asked for the newer file format and was
named by the package report, yet the IDML copy carried the file's defaults. The walk was
narrow on main as well (see the probe evidence), so this is pre-existing for the older
hidden-list state; it becomes a wrong claim only because the paired PR added the variants and
said every placement counts.

Document::placed_items recurses those stories (depth budget 64, like the detection side) and
a new Document::update_placed_items applies the change where the placement actually is,
copying only the branches holding a match. update_items is untouched, so unrelated commands
keep their reach. Both exports also refuse rather than fall back when a document has a choice
but the walk found no placement to carry it.

Regression: nested_placements_keep_their_layer_choices_in_saves_and_exports runs a placement
anchored in a table cell's text, then in a footnote: the save keeps the choice, the embedded
IDML copy shows every layer, external-only IDML is rejected, PDF/A-2b export succeeds.
2026-10-24 15:49:40 -06:00
skylar taylor-barrick 2927861b32 PDF: apply an explicit all-on layer choice and keep an unrewritable one failing closed
An explicit all-on choice is Some(&[]) for the placed PDF, not the file's defaults, so the
item walk now passes it whenever the placement has a layer choice. Without that, a sheet
that inherits a placed PDF with "show every layer" exported the source's default-hidden
layers instead.

The second half is the fail-closed rule: prepare() skipped writing a group's state when a
visible, named layer needed no change, so an all-on choice on a file whose defaults already
show everything embedded the untouched source - no rewrite, so no error. It now writes every
group's state when a choice is explicit, which is the same catalog rewrite the canvas does,
so a history that cannot take one is reported instead of being embedded.

Regression: the two existing engine tests that were failing (placed_pdf_all_on_is_distinct
_from_defaults_and_survives_exports renders the exported page; explicit_all_on_validates_an
_actual_rewrite_before_editing expects the error for a 33-section xref history).
2026-10-24 15:49:38 -06:00
skylar taylor-barrick 09f9ce50e7 Package: name the placed-PDF layer choices its IDML copy cannot carry
A package's IDML copy links the placed files, so a per-placement Object Layer Options
choice cannot travel in it, while the packaged DesignCraft document keeps the choice. The
report now says so instead of leaving the two copies to differ silently.
2026-10-24 15:37:14 -06:00
skylar taylor-barrick 10b2c15065 Format: a table-cell-only document still needs the version 4 reader
The graphic-layer test covered a mixed document. A save whose only layer settings sit in a
table cell has to choose version 4 too, asserted against the saved metadata.
2026-10-24 15:37:13 -06:00
skylar taylor-barrick 12fd492aa8 M6: an untouched Object Layer Options dialog keeps the file's defaults
A dialog opened without the menu's fields has no useDefaults field, and confirming it with
no layer unticked sent useDefaults:false with an empty hidden list: an explicit all-on
choice, which turns a PDF's default-hidden layer on and makes a PDF/A-2b export rasterise
the placement. An absent field now resets to the file's defaults when nothing is unticked,
and only an explicit false means all-on.
2026-10-24 15:37:13 -06:00
skylar taylor-barrick f09df5123e M6: keep placed-PDF layer choices for parent-page and anchored placements
The IDML layer-variant pass and the PDF/A-2b rasterisation pass both walked only
document-spread items, so a choice on a parent-page placement (or an anchored one) was
replaced by the file's defaults on export, and an external-only IDML export succeeded
when it could not carry the choice. Both now cover every placement through
Document::placed_items, and the choice is cleared where the placement actually is.

Regression: a parent-page placement with an explicit all-on choice keeps all layers in
embedded IDML, external-only IDML rejects it, and PDF/A-2b rasterises it.
2026-10-24 15:37:12 -06:00
skylar taylor-barrick ab8ec71234 PDF: keep both layer readers compiling after the merge
Graphic gained an optional layer_options field, so the test-only literals in the PDF
crate and in the engine's optional-content support need it. No behaviour changes.
2026-10-24 15:37:10 -06:00
skylar taylor-barrick faed705607 Merge contributor branch codex/pdf-layer-explicit-visibility (PR #1671) into the paired integration branch 2026-10-24 15:30:04 -06:00
skylar taylor-barrick 9fc02f8f7b Merge contributor branch codex/placed-graphic-layer-overrides (PR #1647) into main
Conflict resolutions:
- crates/pdf/src/export.rs: keep main's `graphic(.., hidden)` signature from
  #1241 and report retained graphic-layer overrides that drawing cannot apply.
- docs/gaps.md: keep main's change list and add the graphic-layer entry.
2026-10-24 15:30:03 -06:00
skylar taylor-barrick 0e0ed78125 M6: integrate placed-PDF vector layer export with current main (#1241)
Preserve contributor commits and adapt optional-content registration and table-cell graphic calls to the current PDF exporter refactors. Keep current-main roadmap corrections.

All nine cargo xtask ci gates passed on tree d0c21f347e: 3209 tests passed, 6 skipped, 16 WASM-safe crates. The exact PR head also passed 118 PDF unit/integration tests. Independent production-export controls: 8 flat/null cases succeeded; 7 unreadable Prev values refused, 4 compatibility controls succeeded; all 12 emitted PDFs pass qpdf.

This PR is independent of the format-v4 pair (#1647/#1671), which remains held for a reproduced parent-placement IDML issue.
2026-10-24 14:38:11 -06:00
aiongg a16f29a669 Optical margin alignment: lines with a tab hang their edge glyphs, as both line breakers measure them (#1760)
layout_line set a line with a tab or a right indent tab without the optical
margin or CJK edge hangs, while the greedy breaker and the paragraph composer
counted them. A line that fit only because its period hangs was then squeezed
into the measure or set past it.

layout_line now hangs the edge glyphs of every line. Tabs are measured from the
hung start, so tab stops stay where they are, and a right indent tab fills to
the hung end. A tab at a line edge hangs nothing. Lines without tabs lay out as
before.
2026-10-24 14:45:04 -04:00
Brandon Thomas 610792ed34 Merge storytold/main into feat/multi-automation-roots 2026-10-24 10:24:14 -04:00
Brandon Thomas 4b9b0a96f1 Merge storytold/main into feat/convert-variable-to-text
# Conflicts:
#	docs/gaps.md
2026-10-24 10:24:00 -04:00
Brandon Thomas 3ea6459651 Merge storytold/main into feat/layout-go-back-forward 2026-10-24 10:23:44 -04:00
Brandon Thomas 0f3ce4dca6 Merge storytold/main into feat/previous-next-note 2026-10-24 10:23:30 -04:00
Brandon Thomas 073287500f Merge storytold/main into feat/convert-to-note 2026-10-24 10:23:16 -04:00
Brandon Thomas 12c6c721e7 Merge pull request #1753 from Hamza0101/fix/grep-scan-bounds
GREP styles: a scan and the pattern cache are bounded (#1598)
2026-10-24 10:23:12 -04:00
Brandon Thomas c0e2c608cd Merge pull request #1751 from Hamza0101/fix/linked-file-read-cap
links: a linked file is read under a cap, not whole (#1596)
2026-10-24 10:23:09 -04:00
Brandon Thomas a57d4cea1b Merge pull request #1752 from Hamza0101/fix/import-value-ranges
import: a value a file gives is one the engine can mean (#1593)
2026-10-24 10:23:05 -04:00
Brandon Thomas a67cf73d83 Merge storytold/main into effects-dialog-global-light-angle 2026-10-24 10:22:52 -04:00
Brandon Thomas 520a810640 Merge pull request #1742 from dotCooCoo/fix/package-redirect-test-junction
Package: the Windows redirect test falls back to a directory junction
2026-10-24 10:22:47 -04:00
Brandon Thomas 8d6989c508 Merge pull request #1740 from Silvertree2010/fix/h1918-footnote-numbering-overflows-u32
Saturate footnote and endnote numbering at u32::MAX
2026-10-24 10:22:43 -04:00
Brandon Thomas 178efcccd9 Merge pull request #1732 from dotCooCoo/fix/rooted-link-tests-on-windows
Preflight: the missing-link test uses a path that is absolute on Windows too
2026-10-24 10:22:38 -04:00
Brandon Thomas 5dddce5eb6 Merge pull request #1707 from Silvertree2010/fix/h1903-table-style-altrows-counts-are-t
Saturate table style altRows counts instead of truncating to 32 bits
2026-10-24 10:22:35 -04:00
Brandon Thomas 280436dbdc Merge pull request #1700 from Silvertree2010/fix/h1882-html-epub-style-css-drops-the-un
Join underline and strikethrough into one text-decoration in EPUB CSS
2026-10-24 10:22:31 -04:00
Brandon Thomas 6cf71e33e3 Merge pull request #1699 from Silvertree2010/fix/h1863-from-bezpath-loses-smooth-anchor
Detect smooth anchors on all open subpaths in from_bezpath
2026-10-24 10:22:28 -04:00
aiongg 5def4a8c01 Story Optical Margin Alignment with a size, and Ignore Optical Margin (Story panel, engine and IDML) (#1705)
* 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
2026-10-24 10:08:40 -04:00
Brandon ThomasandClaude Opus 5.5 b8070d814c links: the capped-read test is native-only, like reload_capped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:05:55 -04:00
Brandon ThomasandClaude Opus 5.5 637c32506a GREP styles: empty matches don't count toward the per-paragraph cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:05:52 -04:00
Brandon Thomas b724698d98 Merge remote-tracking branch 'origin/main' into pr1678
# Conflicts:
#	apps/designcraft-cli/src/main.rs
2026-10-24 10:03:25 -04:00
Brandon Thomas b33224d692 Merge remote-tracking branch 'origin/main' into pr1639
# Conflicts:
#	docs/gaps.md
#	docs/target-app-parity.md
2026-10-24 10:02:54 -04:00
Brandon ThomasandClaude Opus 5.5 5fca302ff6 Endnotes: saturate the remaining numbering sites at u32::MAX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:01:04 -04:00
aiongg d767fcf41e Text frames: a custom baseline grid's Relative To (page, margin, frame or inset) (#1758) 2026-10-24 09:55:47 -04:00
aiongg 7848c21220 Paragraph Style Options: Align to Grid (None, All Lines, First Line Only) in Indents and Spacing (#1759) 2026-10-24 09:38:37 -04:00
dotCooCoo 42d2857fcc Type › Notes › Previous Note and Next Note: the insertion point moves from note to note, as in InDesign
The new commands note.previous and note.next move the insertion point
to the previous or next editorial note in its story and put the caret
after the note's anchor. Previous Note skips the note whose anchor the
caret directly follows, so repeated use reaches every note. Each item
is unavailable when no note lies in its direction in the story, or when
the insertion point is in a table cell or a footnote. The items follow
the Notes panel item in Type › Notes, with translations in the catalogs
that already translate Next Document and Previous Document.
2026-10-24 06:17:31 -07:00
Andee CollardandClaude Opus 5.5 aeccc9c5d3 PDF: validate an actual layer rewrite for explicit all-on choices
skylartaylor's review: a PDF can list readable layers yet fail the visibility
rewrite. On a synthetic 33-section xref history, object.layerOptions with
useDefaults:false and an empty hidden list was accepted (only hidden names
triggered #1670's rewrite check), the canvas could not draw the choice and
PDF export then failed.

Every explicit choice now runs one actual pdf_hide_layers rewrite per
distinct source before any item is edited or undo is recorded; this subsumes
the separate layerless check. Resetting to file defaults (useDefaults:true
or the legacy empty list) needs no variant and bypasses the check. #1670's
fail-closed canvas handling is kept alongside the explicit-override state: a
stored explicit choice (including all-on) whose source can no longer be
rewritten draws nothing rather than the unmodified source, and export errors.

The new regression test fails before this change (the all-on request is
accepted) and covers both selection orders, atomicity, undo depth, reset
bypass and the stale all-on fail-closed path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:51:48 +01:00
Andee CollardandClaude Opus 5.5 96a4a9060b Format: test one version 4 reader keeping both layer capabilities
skylartaylor review of #1647/#1671: both PRs introduced native format 4 for
different capabilities. Stacked on #1647, version 4 is one capability:
required_version (the single place that picks a version) asks for it when a
document has retained placed-graphic layer options or an explicit all-on PDF
layer choice, so a version 4 reader understands both. That merge was made
while rebasing "PDF: distinguish all-on layer choices from file defaults",
which also replaced its Document::all_items scan (document spreads only)
with Document::has_explicit_all_on_pdf_layers, covering parent spreads,
groups, anchored objects and nested story content; an all-on PDF on a parent
page previously saved as version 2.

This save/reopen test holds both features in one document, checks each
alone still needs 4, that documents with neither keep 3 (EPS) or 2, and that
a parent-spread all-on choice needs 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:51:37 +01:00
aiongg 4be45a713a Anchored text frames compose against the grid and page where they are placed (#1710)
* 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
2026-10-24 08:30:03 -04:00
andeecollard 57658368af PDF: validate layer overrides and query mixed selections 2026-10-24 13:23:02 +01:00
andeecollard 8d295b101e PDF: distinguish all-on layer choices from file defaults 2026-10-24 13:23:02 +01:00
Andee CollardandClaude Opus 5.5 2633fe8b5d Format: choose the native version in one place; share v4 with #1671
skylartaylor's review: #1647 and #1671 each introduced native format 4 for a
different capability, so a reader that understood only one could accept a
file carrying the other and silently drop its settings. No release has shipped
v4 (main and v0.12.0 still write at most 3), so v4 is defined as one
capability covering both retained placed-graphic layer options and explicit
all-on PDF layer choices; #1671 is stacked on this branch.

Move the version choice into required_version so every new capability is
decided in one place, document the shared v4 contract on VERSION, and test
that plain, EPS-only and layer-option documents keep 2, 3 and 4 respectively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 13:22:08 +01:00
andeecollard b7e36bf616 M6: retain layer order when native parents reopen closed subtrees 2026-10-24 13:22:08 +01:00
andeecollard 3d362d0bdf M6: preserve nested graphic layers and retain export diagnostics 2026-10-24 13:22:08 +01:00
andeecollard 7074dc8a85 M6: preserve per-placement graphic layer options 2026-10-24 13:22:08 +01:00
aiongg d4c1322d97 Merge pull request #1709 from aiongg/w65-batch
Integration batch: book-chapter IDML baseline grids, hyphenation, and the Paragraph panel stack
2026-10-24 08:14:29 -04:00
Hamza d5a6a7ddd5 GREP styles: a scan and the pattern cache are bounded (#1598)
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.
2026-10-24 16:26:39 +05:00
dotCooCoo e51b2c6114 Type › Notes › Convert to Note: the selected text becomes a note, as in InDesign
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.
2026-10-24 04:18:18 -07:00
Hamza 7808816e9a import: a value a file gives is one the engine can mean (#1593)
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.
2026-10-24 16:17:49 +05:00
Hamza 332b0d9f5c links: a linked file is read under a cap, not whole (#1596)
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).
2026-10-24 16:07:21 +05:00