A fill, stroke or stroke gap set to overprint in [Paper] or in a process
colour that lays down no ink (C=0 M=0 Y=0 K=0, gray 0, RGB white, a 0%
tint) disappears on press. Preflight now reports it as whiteOverprint, and
ruby and kenten set to overprint in such a colour too. A spot ink (a white
ink or a varnish) prints whatever colour stands in for it, so it isn't
reported. Text has no overprint of its own, so white text knocks out and
prints, and isn't reported. The old Preflight note that PDF export drops
ruby and kenten overprint is gone: export writes it.
* A hyperlink can point at a text anchor (#1446)
A frame or a text range could link to a URL, an email address or a page
number. A page number is the wrong thing to store for "this thumbnail links to
that photo": the photo moves and the link does not. InDesign stores the
destination, and IDML files do too — a `HyperlinkPageItemSource` pointing at a
`HyperlinkTextDestination` is how a report links its thumbnails to the
full-size pictures (#1446).
`HyperlinkDest::Anchor(id)` is a text anchor, the same destination a
cross-reference targets, and the PDF export resolves it the same way: through
the cross-reference index, to whichever page the anchor's text has landed on.
A link to an anchor that no longer exists is left out of the file rather than
pointing somewhere wrong.
`hyperlink.create` and `hyperlink.edit` take `anchor` — an anchor's id or its
name — and a link that is given no name of its own takes the anchor's.
Measured: a thumbnail on page 1 linked to an anchor in text on page 3 exports
`/Dest [<page 3> 0 R /XYZ 0 300 0]`; insert a page in front and the same link
exports page 4, with no edit to the link.
This is the groundwork for the IDML half of #1446 (import and export of
page-item and text hyperlinks), which is the next change and needs a
destination to put them in.
* An anchor a hyperlink cannot mean is refused, not ignored
This PR added anchors as link destinations and did not check that the anchor
exists, so the feature was wrong on its own and review was right not to take the
fix from a PR stacked above it.
`dest_param` returns `Result<Option<_>>` now. `None` still means no destination
was given, which `hyperlink.edit` needs for an edit that only renames; an
`anchor` that is given and names none is an error.
Two behaviours follow. An unknown *id* was accepted outright -- `as_u64`
answered for any number -- so a link was stored to an anchor nobody has and the
export quietly left it out. And on `hyperlink.edit` the `None` read as "no
destination given", so the command reported success and kept the old
destination, which is the case review reported.
An unknown *name* did already fail on `hyperlink.create`, but with "give `url`,
`email`, `page` or `anchor`", which named the wrong problem; it says "no text
anchor X (anchor.list names them)" now.
The test covers the id, the name, both on `edit`, that a refused edit leaves the
destination, the name and the exported page alone, and that a rename naming no
destination still works -- that last one is what `None` is for and it must not
have become an error on the way.
The page-number validation #1564 adds stays there; this is the anchor half,
which belongs with the anchor feature.
* A name two anchors share cannot pick one of them
`anchor.create` does not require an anchor's name to be unique, and an IDML
import can bring a document that already has two of the same. The lookup took
the first match, so a link made on a shared name went to whichever anchor came
first -- a different page, with nothing to say so (noted in review).
An id still names one anchor or none, and is checked as before. A name that two
anchors answer to is refused, with their ids in the message so the caller can
say which one they meant.
Not uniqueness at creation, which was the other way offered: a document can
already hold duplicates, so enforcing it there would refuse to open files that
are fine today, and the ambiguity only matters where a name is being resolved.
The test builds two anchors of one name on different pages, asserts both
`hyperlink.create` and `hyperlink.edit` refuse it and say what to do instead,
that the refused create made no link and the refused edit left the destination
alone, and that each anchor is still reachable by its id. It is a test of its
own because the twin anchor outlives the deletion the other test ends with.
* Test: two anchors sharing a name come from a file, now that anchor.create refuses a name in use
main's anchor.create now refuses a second anchor of a name, so the test makes the first and renames the second in the document, as a file opened with two can hold them. The comment on the guard says the same.
* An item a parent overrode and deleted is gone from the pages that use it (#1469)
In InDesign, removing an inherited item from a parent page means overriding
it there and deleting the override: the parent's page keeps the item in its
override list with no copy of its own. Only the page being drawn was checked,
so the pages that use that parent still drew the base's item -- a signature
block from parent B over the notice text of every page using parent E.
The override lists of the whole parent chain are checked, not just the page's
own. An override always names an item of a parent further back, so one set
covers the chain.
Rebased onto the post-split main: the parent-item loop moved from render's
lib.rs to draw.rs and the tests to tests.rs.
One thing this does not touch: blendspace.rs checks `page.overridden` the same
way in two places, for deciding which spreads hold transparency. A deleted
override there would count an absent item as present, which can only cost an
unnecessary flatten rather than draw the wrong thing, so it is left for a
maintainer to judge rather than widened into this fix.
* Parent items: one set of overridden items for drawing, PDF and PPTX export, hit-testing, snapping and frame edges
* Override All: an item a parent removed is not brought back as a copy
Esperanto in Character › Language (panel, Properties, style options),
mapped to the BCP 47 tag eo, with English-style typographer's quotes.
IDML keeps the name as written ($ID/Esperanto). Not hyphenated yet:
morpheme-based hyphenation follows in #1201.
Claude-Session: https://claude.ai/code/session_01Y9j7XJDCmPc8UZLs9cTxtu
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
From review: a copied graphic keeps no story, so there is nothing to scan; and anchored_items already lists a group's members, so walking each item again visited them twice through unbounded recursion. A text frame inside an anchored group (as IDML import anchors an InDesign group) is tested.
clip_doc kept the stories of the text frames among the copied items, and a text frame anchored in a frame's text (as IDML import makes them) is in its host's story, not among them. Its story was dropped from the clipboard, and the paste gave the anchored frame a new empty story. The stories of text frames anchored in the kept stories, and in theirs, are kept too.
* Pages: more parents and alternate layouts as columns in the Pages panel
Parents
- layout.parents.duplicate copies a parent spread (pages and items) under the next free prefix.
- layout.parents.options also renames the prefix (1-4 characters, no "-") and the name; basedOn is now optional.
- layout.parents.new validates prefix and name and takes basedOn.
- Pages panel: a "+ New Parent" row adds B, C… in one click. The parents' context menu and the panel menu have New Parent…, Duplicate Parent Spread, Parent Options… and Delete Parent, named after the selected parent, or a submenu of the parents when none is selected. A new Parent Options dialog has Prefix, Name and Based on Parent.
Alternate layouts
- Section.layout marks the section that starts a layout; Document::layouts() lists them.
- layout.createAlternate copies one layout (from), not every page; new layout.alternate.duplicate, .delete and .options (rename, resize).
- Each layout starts a spread of its own; sections now follow their pages when pages are inserted or deleted.
- Pages panel: with more than one layout, each is a column side by side with a header (name, size, ▾ menu: create, duplicate, options, delete) and a + column to add one.
Tests cover the commands' happy and error paths, undo, serde, and the panel's menus and columns.
* Pages: address review of parents and alternate layouts
- Deleting a layout's first page keeps the layout's name when another section lands on the same page.
- A parent's full label (prefix-name) must be unique; parents may still share a prefix.
- Alternate layout names round-trip through IDML (Section AlternateLayout / AlternateLayoutLength).
- The Pages panel's selected parent is per document and no longer saved in ui.json.
- Layout ▸ Parent Options… and Alternate Layout Options… open the Pages panel's dialogs.
- The Pages panel computes the layouts once per frame.
* Export: layouts and versions in one export, with presets; print chosen layouts
- file.exportBatch writes the chosen alternate layouts (a file per layout, or all in one PDF) in several versions at once: PDF with any marks, bleed and standard, PNG or JPEG at a resolution. Each version goes in a folder of its own, and file names come from a template ({document}, {layout}, {version}, {date}). Names are made safe and unique. dryRun lists the files; without a dir the files come back as base64 (web).
- Export presets: file.exportPresets, file.saveExportPreset, file.deleteExportPreset, kept in prefs.json. Built-ins: Print + Screen, Press PDF per Layout, Images per Layout, All Layouts in One PDF.
- file.print takes layouts and separateJobs (a job per layout, titled after it).
- File ▸ Export Layouts and Versions… dialog: preset, layouts, versions (add from templates, duplicate, remove, quick PDF choices or custom marks), name template, and a live list of the files it will write. It opens from a layout column's ▾ menu as "Export Layout…". The desktop asks for a folder; the web downloads each file.
- The Print dialog prints chosen layouts, together or separately; the Export PDF dialog fills the page range from a layout.
- crate::io::create_dir_all keeps folder creation inside the automation write root.
* Export: address review of layouts and versions
- An exported image is capped at 80 million pixels, the limit EPUB page images already use.
- The Print dialog stays open and says why when no layout is chosen.
- Clicking a layout column's header leaves Edit Parents before going to the layout's first page.
* Pages, Export: address maintainer review of layouts and versions
- Sections (and the layouts they start) follow a moved page, as they do inserted and deleted
ones: moving a page across a layout boundary no longer moves the other pages between layouts.
Delete and move share `settle_sections`.
- Alternate Layout Options resizes only pages whose size changes: renaming a layout no longer
forces every page to the first page's size.
- Export file and folder names that are Windows device names (CON, NUL, COM1…) get a leading `_`.
- An export without `dir` refuses past 512 MB of files instead of building them all in memory.
- A file that fails partway through an export to a folder says how many were already written.
- Parent Options and Print keep the dialog open, with what was typed and why, when the engine
refuses.
- A duplicated layout's default name stays within the 100-character limit.
- rustfmt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pages: Alternate Layout Options sends the size only when it changed
The dialog starts at the layout's first page's size, so the engine couldn't tell a rename from a
resize to that size. The dialog now leaves out a width or height still at its starting value, and
`layout.alternate.options` resizes every page of the layout when a size is given, as before.
The test moves to the UI, where the size was coming from.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rename_swatch_refs visited every page item's fill and stroke but not the swatch applied to a placed greyscale or bitmap image (Graphic::fill), framed or in a table cell. Those images kept the old name, found no swatch, and drew in black.
The nonprinting stand-in for a rasterized linked frame keeps the frame's
story, alt text and effects for link bounds and text composition. A test
now checks that a tagged PDF of a linked frame differs from the same frame
without the link only by the link annotation: no extra structure elements
or alt text. Note why the stand-in keeps the frame's ItemId.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Include the PDF export warnings in the package result and report. Cover
shadow and feather output in packaged PDFs with a synthetic regression test.
Preserve frame and text-range hyperlinks on rasterized frames, including
inside groups, at their original source bounds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Placing an image or media with no placement parameters used the selected
item as the frame, and so did a click of the loaded place cursor. On a
line that made a zero-height frame that kept its stroke. All three now
skip items that enclose no area (PathData::encloses_area): lines and
paths whose points all lie on one line get a new frame instead. Frames,
and open paths that enclose an area, take the content as before.
An explicit frame, x, y, spread or width behave as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Frames holding pasted-in images are hit inside the frame, take a placed graphic; documents open on a usable layer
A frame with items pasted into it (how a document from InDesign keeps a cropped photo) was hit
through its children like a group, so the whole uncropped image counted: a click on the empty
page beside such a photo selected it, and nothing could be deselected there. It is now hit by
its own area, as it is drawn clipped. Such a frame holding only graphics also takes a placed or
dropped graphic (Item::takes_graphic), which fills it as an empty or graphic frame is filled.
A document opened with new objects going to its top layer even when that layer was hidden and
locked (a dieline), so a placed graphic showed as an empty frame outline. It now opens on the
topmost layer that is shown and unlocked (Document::working_layer).
* Place: a graphic replacing the one a frame shows keeps the frame's crop and zoom
Placing or dropping into a frame that shows a graphic (in it, or as the one graphic pasted into
it) filled the frame afresh, so the crop and zoom chosen for the old picture were lost. The new
graphic now covers the old one's area, centred on it: an edited copy of the same picture lands
exactly where the old one was.
* Move the replace-keeps-crop and open-on-a-usable-layer changes to their own PRs
Review asked for them to be checked against reference app behaviour on their own. This PR keeps
the frame hit area and Item::takes_graphic. Reverts 8ea0bc16 and drops Document::working_layer.
* Move Document::working_layer and its test to #2267
A document opened with new objects going to its top layer even when that layer was hidden and
locked (a dieline), so a placed graphic showed as an empty frame outline. Document::working_layer
picks the topmost layer that is shown and unlocked (the top layer when there is none); DocState::new
and the fallback after the active layer is deleted use it.
The warning from #2190 looked only at the attributes passed. A composite
font made by style.compositeFont.set isn't an installed family, so
applying one warned "Missing font: Mixed" even when its entries were
installed. And a fontStyle-only call, or a family change that kept a
style the new family lacks, said nothing.
After a call that sets or clears fontFamily or fontStyle, type.char now
tallies the fonts the formatted text resolves to, with the code
font.list uses (composite entries per character), and warns about the
missing ones and substitute styles. At a caret it checks the font the
next typing gets. tally_story becomes tally_range over a byte range,
and describe_fonts is the part of used_fonts that type.char reuses.
A relative link names another file once the document is saved elsewhere,
so a status remembered for the old file must not answer for the new one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fontFamily the app doesn't have was applied with a null result, so a
script that asked for Courier Prime exported the PDF in the substitute
and exited 0. The font is still applied, since documents can carry
missing fonts and Find Font and preflight list them, but type.char now
returns {"warnings": ["Missing font: Courier Prime (using Source Sans 3)"]},
or "Missing font style: ..." for a style the family doesn't have, in
preflight's words. Other calls still return null.
links.list decided a placed graphic's status by reading the whole file and comparing it with the asset's bytes whenever the sizes matched, and the Links panel polls it twice a second, so a document of press-size images read every one of them in full on every poll. Image links now have the size-and-time fast path linked text files already had: a file of the remembered size and time isn't read, a new time is read once and remembered (unless the file changed in the last two seconds), a different size is modified without a read. The answer is keyed by the asset's bytes' identity, so a relink, update or undo can never be served a stale one; status() stays as the exact check for callers that want it.
Verified locally on a288a87b: fmt, clippy, engine link tests including the new unchanged_linked_images_are_not_read_again.
A sheet draws its spread's items without asking which page each one sits on, so
a frame straddling two pages is drawn, in part, on both. The warning chose the
page from the frame's centre alone (`page_at_x(bounds.center().x)`), so a frame
centred on a page the export leaves out had its warning dropped while the part
on an exported page went into the file (noted in review).
`pages_of` gives every page of the spread the frame's bounds overlap, and the
story is kept when any of them is exported.
The page named is one the export has: the centre page when that is exported, so
an ordinary frame reads exactly as before, otherwise the first exported page the
frame reaches. A frame wholly on the pasteboard overlaps no page's span and
falls back to `page_of`, so the two cannot disagree about where such a frame is.
`overset_location` returns the reached pages beside the centre page, which is
what made it worth a named type.
The test exports a frame centred on page 3 and reaching back into page 2, four
ways: the whole document, page 3 alone, page 2 alone -- which used to report
nothing -- and page 4, which the frame never reaches and which still reports
nothing.
Found while testing crates/engine/src/cmd/datamerge/parse.rs:212 (parse_record_no, used by select_range)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merge origin/main at a288a87b. Preserve the font.list progress entry and the content-first routing note. The feature diff remains the same four reviewed files.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep native ZIP, legacy JSON and IDML bytes out of the INDD converter even when a browser supplies a misleading .indd or .indt name. Preserve signature and unknown-data routing to the converter. Check that every browser delivery adds a document before asserting its title.
Co-Authored-By: OpenAI Codex <noreply@openai.com>