The PDF exporter called Cache::get twice for every text frame it drew.
Each call rebuilds the cache signature of the whole story, which looks up
every frame of the story with Document::find (a linear walk over all
spreads), and the anchored-frame placements walk them again. For a story
threaded through many frames this is O(frames^2 x items) per export.
The document doesn't change during an export, so the exporter now keeps
the composition it got for each (story, page) and reuses it. The cache
still decides what to compose; the output is byte-identical.
Add examples/pdf_export.rs to designcraft-cli: File > Export PDF through
a headless session on the `designcraft-cli perf` stress document (200
pages, 1M characters in 40 threaded stories, 500 placed images). The
stress document generator moves unchanged from src/perf.rs to a small
library module (src/stress.rs) that the binary and the example share.
Ryzen 16 threads, Linux, 12 runs each, interleaved:
main median 3485 ms, min 3427 ms
patched median 2016 ms, min 1656 ms (-42 % median, -52 % min)
The exported PDF is byte-identical (12,075,744 bytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(engine): split convert_all's per-story plan into convert_plan
Resolves SonarCloud rust:S3776 on convert_all (cmd/variables.rs).
* refactor(pdf): split graphic_unclipped into per-kind helpers
Resolves SonarCloud rust:S3776 on graphic_unclipped (pdf/src/export.rs).
* refactor(compose): extract wrap exclusions and frame text area from frame_specs_placed
Resolves SonarCloud rust:S3776 on frame_specs_placed (compose/src/lib.rs).
* refactor(ui): extract command shortcut lookup from shortcut_key
Resolves SonarCloud rust:S3776 on shortcut_key (ui-egui/src/menus.rs).
Brings in #1374 with upstream main merged (egui 0.37, craft-libs at
storytold/craft-libs e3b9e239).
Conflicts:
- Cargo.toml: print-color's pins (storytold/craft-libs e3b9e239 for
craft-pdf, craft-prepress, krilla and the hayro crates; winit and egui
patches), with craft-prepress keeping the `flatten` feature.
- xtask/src/pdf_deps.rs: print-color's REPO/REV.
- Cargo.lock: print-color's, relocked with cargo metadata; adds only
craft-prepress and linesweeper.
- crates/pdf/src/export.rs: main's optional-content registration and
add_form_fields helper; the helper leaves form fields and media out of
PDF/X-1a with a warning.
- crates/engine/src/cmd/export.rs: the X-1a parameter docs plus main's
`created`; the X-1a flatten and shadow-mask path on top of main's
PDF/A-2b-only rasterize_layered.
Also: x1a_tests builds FlattenTotals through Default, since the struct is
non_exhaustive at e3b9e239.
print_pdf, shared by Export PDF, File › Print and Separations Preview, takes main's two
changes to the inline export pipeline: rasterize_layered runs only for PDF/A-2b (other
standards keep hidden placed-PDF layers as optional content), and the report gets
GRAPHIC_LAYER_RENDERING_WARNING when the source document has graphic layer visibility
overrides and the writer did not report it. The Separations Preview command matches
craft_prepress::Conversion with a fallback arm, since the enum is non-exhaustive at
craft-libs e3b9e239.
PDF/X and CMYK groups keep this branch's approach: krilla's serialize
settings (pdfx::print_settings) write the CMYK page and transparency
groups, the GTS_PDFX output intent and the PDF/X Info and XMP entries.
make_pdfx4 adds only the XMP dates, so main's title argument and its
output-intent, Info and pdfxid XMP edits are dropped, and both
cmyk_group_spaces calls after writer.finish() (Export PDF, Print
Booklet) stay removed. Main's new parts are kept: file_info_metadata
(also for Print Booklet, on top of the print settings), optional content
registration and layer errors, add_form_fields, and pdfx's fonts module.
Main's metadata panic test calls the two-argument make_pdfx4.
craft-libs and the krilla patch are pinned to storytold/craft-libs
e3b9e239; Cargo.lock is main's, relocked without updates.
A column rule ran from the top of the columns to their bottom, so a frame
that the text only half fills drew its rules down to the empty bottom of
the frame. InDesign draws them from the top line's ascent to the bottom
line's descent. column_rule_rects now takes that extent from the frame's
lines, and the top and bottom insets shorten the rule from there. A frame
without lines draws no rules. Existing documents draw their column rules
at the new length, on screen, in PDF and in PowerPoint export.
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.
Every craft-libs crate and the Krilla/Hayro patches use one revision, which
also vendors hayro-postscript. Clipping text paints nothing in the PDF bounds
device; its clip arrives through push_clip_path.
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).
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.
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.
A placed file whose catalog has /OCProperties null (literal or through a
reference) exported on main but failed after the layer-choice change; treat it
as absent. Like the trailer guard in #1670, a non-null /Prev that can't be read
as an offset ends nothing and is reported as a damaged cross-reference table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Export takes the placed PDF's layer choice as file defaults or an explicit
hidden list, where an explicit empty list shows every layer, including one
the file hides by default. Today an empty list still means file defaults;
an explicit all-on state can map straight onto Some(&[]).
The incremental writer now reads each cross-reference section's trailer by
walking the table rather than searching for the keyword, follows the bounded
Prev chain for inherited encryption, and treats a literal null /Encrypt as
absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now aborts an export when a stored hidden layer can no longer be
applied (#876). Vector export follows it: a layer hidden in Object Layer
Options that the placed PDF doesn't have, because the file has no layers
or was relinked, fails with an error naming the file instead of exporting
everything visible. The image-path tests for oversized images and
undecodable content export as PDF/A-2b, which still rasterizes placed
PDFs with hidden layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep source defaults and per-frame hidden layers in plain and PDF/X-4 vector exports, including independent repeated placements, booklet output and book merging. Accept usage-application arrays and resource-only groups without a catalog configuration.
Use file-named warnings and all-ON defaults for recoverable default-configuration problems and unsupported base states. Add readable groups omitted from OCGs without discarding their visibility, combine concurrent recovery warnings, and generate missing names only when no frame override needs name matching. Retain errors for contradictory states, unresolved references and ambiguous name overrides.
Cache prepared variants per asset and hidden-name set while replaying recovery warnings through the existing deduplicating channel. Keep original source bytes when every group is visible and named, so reader-repaired cross-reference tables remain usable without an incremental layer update. When hidden groups or generated names require writing, reject an unusable cross-reference table with a file-named damage error and retain the first placement failure.
Cover visibility, recovery, names, independent placements, repaired cross-reference tables and vector output with 46 optional-content integration tests, plus preparation-cache and shared-buffer unit tests.
File › File Info… opens a dialog with Document Title, Author,
Description, Keywords, Copyright Notice and Copyright Info URL, and the
document saves them. The new file.info command reads them with {} and
sets the fields it is given; "" or null clears a field, and keywords
are a list or one line split at semicolons and commas. file.info.get
returns the fields without editing the document, so opening the dialog
does not end a Data Merge preview.
Export PDF, PDF/X-4 and Print Booklet write the title, author,
description and keywords into the PDF's document information and XMP.
EPUB export takes the title and author from File Info, and HTML export
takes the title. A title or author given to the export replaces File
Info's, and an empty Document Title leaves the document's name as the
title. Export PDF's dialog no longer sends the document's name as the
PDF title, so the PDF takes its title from File Info; the file name
still comes from the document's name.
- Write GTS_PDFXConformance (PDF/X-1a:2003) in the document information and
XMP; check_pdfx1a requires it.
- A registered printing condition (Coated FOGRA39) has no DestOutputProfile,
so no ICC v4 profile goes into the PDF 1.4 file; an unregistered condition
needs an ICC v2 profile, else the export fails.
- Images the flattener can't read fail the export, naming the page and its
graphics.
- Shadow mask coverage (with - without) / (255 - without), so partly
transparent objects drawn over their shadow give the rendered pixel back.
- The dropped-inks warning says their tints were dropped and won't print.
- Type 3 fonts pass; their glyph procedures are checked like page content.