Appending only checked each batch against the cap, and a single image or a PDF
added with append wasn't checked at all, so repeated appends could grow the
cursor without limit. Refused before decoding a batch or adding an asset.
With a graphic frame selected, `file.place` chose that frame before it looked at the place
cursor, so a PDF confirmed with `append` filled the frame and left the images around it queued.
An appended file now skips the selected-frame fallback; an explicit `frame` still applies, and a
single placed file still fills the selected frame.
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>
Review fix for #2309: set_active, add_document and close_document emptied the place cursor but
kept the placeGun tool, so the next canvas click failed with 'nothing loaded in the place
cursor'. One helper, Session::unload_place_cursor, empties the cursor and its queue and goes back
to the Selection tool when the place cursor's tool is active; validate_place_cursor uses it too.
Test: changing_documents_unloads_the_place_cursor_and_its_tool.
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.
Split out of #2073. Port of #1307, #1392 (grid) and #1432 (thumbnail and count).
- File › Place takes several files; images load the place cursor in the
order chosen (`place.load` `files`, `append`), after the ones it holds.
A multi-page PDF or an alert stops the batch, and a file that can't be
placed returns its error to `app.placeDialog`.
- Each click places the next image; arrow keys while dragging lay out a
grid (`place.grid`) and are handled even at a limit; Escape unloads the
rest (`place.cancel`). Undo of a loaded image's asset cancels the queue.
- The cursor shows how many images are loaded.
- On the web, Place picks several files and the inbox loads consecutive
images for the same document as one batch.
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>