The warnings composed every story once with no page. For a frame on a parent
page that is wrong twice over, and I measured both:
exportPdf {"pages": [2]}
["the story in frame 9: 76 character(s) do not fit and are not in the
export"]
Page 2 used no parent at all, so nothing of that frame was in its export to be
missing -- and the warning named no page, because a parent frame has no
document page of its own. It now resolves the pages that draw the parent page
and reports only those:
{"pages": [1]} ["page 1: 76 character(s) ... frame 9 ..."]
{"pages": [2]} []
The second half is page-dependent text. A parent frame 112 pt wide holding
"Continued on page <marker>" fits while the number is one digit and does not
once it is two, so composing it once could not tell:
{"pages": [1]} []
{"pages": [9]} []
{"pages": [10]} ["page 10: 1 character(s) ..."]
{} ["pages 10, 11, 12: 1 character(s) ..."]
Each page that shows the parent is composed as that page, and the pages are
grouped by how much they lose, so a story still gets one line.
Also from review: `story.get` now says `overset` is a UTF-8 byte offset into
`text`, not a character index. A test measures the difference on French text,
where the offset runs ahead of the character count.
Review caught a consequence of the previous commit. Standing the order check
aside so a missing value could be reported meant the value was reported
later -- after the exports before it had already been written. `run --export
first.png --scale --export second.png` wrote first.png and only then failed,
so an invocation that fails could overwrite a file that was already there.
`values_present` checks every option that takes a value before the order
check and before any export, so the line is refused as a whole. The test
writes a file at the export path first and asserts its bytes are unchanged.
Found while testing crates/geom/src/stroke_style.rs:24-31 (offset_path: `(MoveTo(p), 0, true) => out.line_to(p)`; also samples/wavy at :58-98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recognize U+00AD independently of automatic hyphenation in the greedy breaker, reusing the existing visible hyphen output. Cover both composers, zero-width unbroken soft hyphens, pattern suppression, and tagged and untagged PDF text extraction.
Fixes#1955
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test for a raster past the RGBA budget showed that recompress
skips the decode only through a one-second limit, which a slow CI
runner could fail and a fast machine could pass without the fix.
recompress now counts its decodes in a test-only per-thread counter,
and a unit test checks that the 16,385 px PNG doesn't reach the
decode while a 9 px one does. The export test keeps its output
checks and drops the time limit.
insert_with_raw gave each return in the inserted text its own
Vec::insert of the paragraph's format, and each insert moves every
format after it. A ParaFormat is 2,888 bytes, so the time grew with
the square of the returns: Story::with_text took 2.2 s for 10,000
returns, 15.8 s for 20,000 and 90 s for 40,000. Placing a plain-text
file goes through it. The copies now go in with one splice.
With compressImages on, PDF export re-encodes rasters as JPEG in
recompress, which decoded with the image crate directly. A gray PNG
that decode_rgba refuses still decoded there, was copied and encoded
as JPEG, and only then dropped: krilla refuses a JPEG wider or taller
than 16,384 px, and every raster past the budget is. Exporting the
23,000 px PNG that way took 4.2 s and 1 GB more memory, for a PDF
without the image.
recompress now asks too_large_to_decode first, the check decode_rgba
uses, so the export skips the image with the same warning at once.
fit() counted every break character in the overset text and every
Start Paragraph option. A break followed by a paragraph whose option it
already meets moved the text once but asked for two frames, and breaks
that tracked changes delete or a hidden condition hides asked for
frames that composing never uses. Autoflow added those pages and
removed them again: 200 chapters that end in page breaks and start on
the next page took 61095 ids and 430 ms, now 798 ids and 16 ms.
Each paragraph composing places now counts once, when a visible break
ends the paragraph before it or its start option sends it on; after a
break it is at the top of a frame, where the option only fixes odd or
even page parity. A visible break inside a paragraph counts once more.
previous_visible_break and the new visible_break in designcraft-compose
are public, so the estimate and the composer use one rule.
InDesign always writes a document-level StoryPreference in
Resources/Preferences.xml; DesignCraft only wrote one per story. IDML readers
that take the spread reading order from the document's preference, such as
Okapi's IDML filter, failed on DesignCraft's files without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decode_rgba relied on the image crate's 512 MiB limit, which bounds
only the decoder's own output. A gray or 1-bit image just under it
expanded four times to RGBA, and render made a premultiplied copy on
top: a 288 KB PNG of 23,000 x 23,000 pixels froze the first draw for
9 s and took the app to 4.5 GB. The size is now read from the header
and anything past 256 Mpx is refused before decoding. It draws as an
empty frame, and PDF export skips it with a warning.
A 300 ppi scan of an A0 sheet (about 139 Mpx) still opens.
Reserve column and spanning footnotes when fitting inline table row groups. Continue rows in the next column or frame when the remaining space is too small, and include cell-local footnotes in measured row height.
Fixes#1901
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The start option lookup now reads the next paragraph composing places,
past tracked-deleted ones. Frames that start options and breaks force
are counted and asked for in one round, so a story with a paragraph per
page no longer gets one frame per round.
fit() counted the frame before a paragraph that starts on the next
column, frame, page or odd/even page as a full frame. With a short
title before such a paragraph, autoflow estimated a page per few bytes
left and added up to 2000 pages before removing them again: placing
2000 words took 35.6 s in the app, now 0.36 s, with the same pages.
The option is resolved through the paragraph style, so an imported
Word style counts the same as a local override.
Copying a text frame cloned its story but left the objects anchored in
it with the original's item ids. Save does not validate, load runs
Document::check and rejects duplicate ids, so a duplicated or pasted
frame with an inline graphic produced a file that failed to open with
"not a DesignCraft document: duplicate item id".
renumber (Duplicate, Alt-drag copy, Paste, Paste into text, parent item
overrides) and the data merge copy now give every anchored item in the
copied story, its table cells, footnotes and endnotes a fresh id.
Anchored text frames from IDML get their own story copy. Assets are not
adopted again: the story copy already remapped them.
The IDML export tests that relied on duplication to create shared ids
now set the shared id explicitly, so the export guard stays covered.
Files already saved with duplicate ids still do not open.
* Show file and message dialogs without blocking the frame
A blocking rfd dialog inside the egui frame runs AppKit's modal loop; a
file dragged from Finder and held over the window then re-enters winit's
handler and aborts the app. Dialogs now go through craft-dialogs: a
command without a path asks with a continuation and returns, the host
shows the dialog at the end of the frame, and the continuation runs with
the answer on a later frame, in the document it was asked for. Commands
given a path stay synchronous. The replace-file question is a message
box from the same queue. Quit and Close wait for a Save As dialog's
answer. clippy.toml disallows rfd::FileDialog and rfd::MessageDialog.
* Raise the open file panel when File > Open is chosen again
* Repin craft-dialogs to the craft-libs PR head
* Queue dialogs asked by different code or documents; hold answers behind newer dialogs
Two asks for the same file dialog were treated as one, so Place for one
document and then Choose Image, or Place again for another document, ran
the first ask's action. An ask is now skipped (and the open panel
raised) only when the same code asked for the same dialog for the same
document. An answer that would replace an egui dialog opened since the
asking (an unsaved-changes question, Preferences) waits until it closes;
Quit counts it as pending.
* Repin craft-dialogs to the craft-libs PR head
* Allow the file dialogs' existence checks in the file guard
* Repin craft-dialogs to storytold/craft-libs main
* Honor CARGO_BUILD_TARGET in FreeBSD packaging for aarch64 release builds
The shared release workflow cross-compiles FreeBSD aarch64 in an x86_64 VM and sets
CARGO_BUILD_TARGET. package.sh ignored it: it built for the host, read binaries from
target/release, took the arch from uname -m (amd64 in the VM), and ran the cross-built
designcraft-cli --version on the host.
Take the arch from the target, build and read binaries from the target's directory, and skip
the host-side --version run when cross-compiling. Unset CARGO_BUILD_TARGET keeps the old behavior.
* Pick FreeBSD archive arch from CARGO_BUILD_TARGET with a release_arch helper
Follow soundcraft's pattern: native builds use uname -m, aarch64-unknown-freebsd and
x86_64-unknown-freebsd map to their arch, and any other target exits 2. The --version check
runs only when the archive arch matches the host arch.