chore: sync upstream (145 Commits) #14

Merged
alf merged 145 commits from chore/sync-upstream-20261030 into main 2026-10-30 07:58:50 +00:00
Owner

Upstream-Sync von upstream/main (145 Commits). Fast-Forward.

  • ea08e4c2 UPD.1: honor app pretend versions in update checks (#2025)
  • 15d13dde Update prompt shows the app icon and accent color instead of a letter (#2023)
  • faccd431 CI: workflows only call craft-common-actions at @main (#2018)
  • baab062e Merge pull request #2021 from storytold/docs/roadmap-order-1029
  • e7e780ec docs: keep ROADMAP progress log newest first after batch landing
  • 34697fcb Merge pull request #1879 from PiMinusMax/inner-effects-follow-image
  • a1e9bea4 Merge storytold/main into PR #1879
  • 8636f426 Merge pull request #1985 from VoiceLessQ/fix/1588-story-newlines
  • ee50a741 Merge storytold/main into PR #1985
  • 3a11abcd Merge pull request #1990 from KnottyDyes/squad/discretionary-hyphen-breaks
  • 56decd96 Merge storytold/main into PR #1990
  • 7137006d Merge pull request #1238 from seanth/inline-objects-under-text
  • b7e5b8f9 Merge pull request #1952 from Silvertree2010/fix/h3606-tagged-text-import-accepts-a-sig
  • 7e841a07 Merge pull request #1972 from VoiceLessQ/fix/1209-autoflow-parity-runaway
  • 9d9d3ab1 Merge pull request #1893 from Silvertree2010/fix/h2824-xml-mapstyle-accepts-an-invalid
Upstream-Sync von `upstream/main` (145 Commits). Fast-Forward. - ea08e4c2 UPD.1: honor app pretend versions in update checks (#2025) - 15d13dde Update prompt shows the app icon and accent color instead of a letter (#2023) - faccd431 CI: workflows only call craft-common-actions at @main (#2018) - baab062e Merge pull request #2021 from storytold/docs/roadmap-order-1029 - e7e780ec docs: keep ROADMAP progress log newest first after batch landing - 34697fcb Merge pull request #1879 from PiMinusMax/inner-effects-follow-image - a1e9bea4 Merge storytold/main into PR #1879 - 8636f426 Merge pull request #1985 from VoiceLessQ/fix/1588-story-newlines - ee50a741 Merge storytold/main into PR #1985 - 3a11abcd Merge pull request #1990 from KnottyDyes/squad/discretionary-hyphen-breaks - 56decd96 Merge storytold/main into PR #1990 - 7137006d Merge pull request #1238 from seanth/inline-objects-under-text - b7e5b8f9 Merge pull request #1952 from Silvertree2010/fix/h3606-tagged-text-import-accepts-a-sig - 7e841a07 Merge pull request #1972 from VoiceLessQ/fix/1209-autoflow-parity-runaway - 9d9d3ab1 Merge pull request #1893 from Silvertree2010/fix/h2824-xml-mapstyle-accepts-an-invalid
alf added 145 commits 2026-10-30 07:58:48 +00:00
When InDesign is installed it exports its own type for .idml, which
overrides the type DesignCraft imports, so the IDML entry in
CFBundleDocumentTypes no longer matches: Finder refuses the drop on the
app's icon and Open With leaves DesignCraft out (Dock drops still work).
Add a separate entry by extension, which matches whatever type the file
has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
A multi-column page's "last on page" picked whichever matching paragraph sat
lowest, so a dictionary page whose final entry tops column 2 showed the last
entry of column 1. Hits on a page now order by their story's first position
on the page (top-left), then by position within the story.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
Anchored objects were painted after all of their frame's text, so an inline
picture whose frame reaches below its baseline (a negative Y offset, common in
IDML) covered the top of the next line with its white background. InDesign
paints them under the text. Both the renderer and the PDF export now draw a
frame's anchored objects after its paragraph shading and before its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
Review fix (CodeRabbit): a story was placed on the page by its highest
hit, so a heading at the top of its later column put the whole story ahead
of another story whose heading sits higher than the first one's. Each story
is now placed by its first hit there in text order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
Review request (#1238): a host frame at reduced opacity. The anchored box is
painted inside the frame's transparency group, after its fill and before its
text and stroke, as the screen renderer already paints it on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
# Conflicts:
#	ROADMAP.md
# Conflicts:
#	ROADMAP.md
Resolve documentation conflicts while preserving upstream updates and the PR's German 2006 implementation. Validation: formatting, PDF dependency check, workspace Clippy, asset attribution, layering and German composition tests pass. Full CI reaches four existing Windows/path-related engine test failures in unchanged upstream files.
Merge upstream/main e74208dd into the working branch and retain PR #1254 unchanged. Resolve ROADMAP.md by preserving both progress histories.

Validation: fmt, pdf-deps and workspace Clippy passed; compose 364 passed, 1 ignored, including all 4 German tests. Full CI stops at 4 pre-existing Windows engine tests (paths and symlink privileges); their sources match upstream. Assets check flags the two untracked local chronicle documents.
Merge upstream/main 4be45a71, preserving German 2006 hyphenation. Keep current documentation summaries, both progress histories, and the new cross-column hyphenation behavior description.

Validation: fmt, PDF dependency checks, workspace Clippy, layers and German hyphenation tests pass. Full CI stops at four known Windows engine test failures in unchanged upstream paths and directory-redirect checks.
# Conflicts:
#	docs/typography-parity.md
Preserve the German 2006 hyphenation entry alongside the newer optical margin alignment documentation. Merge upstream main at 5024d61ce6.

Validation: cargo xtask ci passed all nine steps; 3271 tests passed, 6 ignored; all four German hyphenation tests passed; WASM check passed for all 16 crates.
craft-print at a0916808 leaves the color feature out of the Windows
PrintTicket unless the job asks for grayscale, so the color chosen in the
driver's preferences applies, and its command preview ends with
color=(driver default). print_builds_the_spool_command still expected
color=color and failed on Windows. It now expects the new preview.

print_submits_the_selected_export_and_reports_refusal also checks that the
job does not ask for grayscale, so the job's color setting is checked on
CUPS too.
Fixes #1835

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/textimport/src/tagged.rs:~93-100 (tagged_text, the `<cFont:{v}>` and `<cTypeface:{v}>` writes)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A placed media file's type goes into the embedded file's `/Subtype` as a PDF
name, and only `/` was escaped. ISO 32000-1 7.3.5 allows only the regular
characters in a name, so a type carrying a space, a delimiter, a comment
character or `#` ended the name early and left the rest as loose tokens in the
file's structure -- in a PDF the user then distributes.

`pdf_name` escapes every irregular byte as `#xx`. The test covers the shapes
that leaked: a space, `<<`/`>>`, `(`/`)`, `[`/`]`, `%`, `#` itself and a
non-ASCII byte, and checks an ordinary type is unchanged.

This is the escaping part of DC-01 only. The larger part of that issue -- the
post-passes finding the catalog, the page tree and pages by text search over
the output and taking the last match -- needs them to stop rediscovering
objects at all, through the xref chain or by carrying the offsets each pass
writes. That is a rework of `transitions.rs` rather than a patch, and the
issue says a reproduction is available to maintainers privately, which I would
want to test against before changing how objects are found.
# Conflicts:
#	docs/typography-parity.md
On a frame holding a graphic, Inner Shadow, Inner Glow (from the edges) and Bevel and Emboss are built from what the object paints instead of the frame's path: the colour everywhere but under the painted pixels moved by the offset, blurred, and kept only where the object paints. A transparent part of an image is outside the object, so the effect lines the image's opaque edge; before, it lined the frame and nothing marked the image's own edge. Frames without a graphic draw as before. Satin and a centred Inner Glow still follow the frame.

Fixes #1622.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canvas set the tool's cursor every frame the pointer was over it,
after the guide drag had set its resize cursor, so the cursor turned
back into the tool's as soon as the pointer left the ruler and the drag
looked as if it had stopped. A ruler guide drag now keeps the cursor
across the guide, as a column guide drag already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/xml.rs:~290 (the xml.mapStyle command; the export is in export_xml)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/textimport/src/rtf.rs:~118-131 (the \' branch of import: `i += 4` and `u8::from_str_radix(&hex, 16)`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/xml.rs:40 (esc, used by para_xml at :68 and the href at :104)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`image_path_tests` was `#[cfg(all(test, unix))]`, so the assertion that matters
most ran only where it matters least: a merge image field naming another
computer must resolve to nothing, because on Windows an outbound SMB connection
hands over NTLM credentials. That refusal was unverified on Windows.

The refusals now run everywhere -- nothing in them is unix-specific, since
`image_candidates` delegates to `craft-docpath` -- and the Windows shapes that
file refuses are asserted where they have meaning: `\\?\UNC\...`,
`\\.\pipe\...`, `\\?\C:\...` and the legacy device names.

Resolution beside the data and the document stays unix-only: it asserts what an
absolute path means, which cannot be checked on Windows from here.

No production code changes. On macOS both cross-platform tests pass; Windows CI
is the first real run of the Windows ones, which is the point.

Audit DC-20 (its testable part; the privacy note about absolute local paths
matches InDesign and is a product decision, not a bug).
`place_image` stat'd and read its file for every record, so a sheet naming one
image in many rows read it once per row -- up to 256 MiB each time. The audit's
case is a 100,000-row CSV naming one large file: about 25 TB of I/O for one
merge.

A merge now carries an `ImageCache`: the bytes by resolved path, read once and
shared. The asset takes the cached `Arc` rather than a copy, so the records
share one buffer as well. The cache stops growing past 256 MB of distinct
files, so a merge over many large images is slow rather than out of memory.

Measured by counting what the engine hands the file system (`io::touched`):

    without the cache   50 reads for 25 records   (a stat and a read each)
    with it             2

A note on the test, because the first one I wrote was useless: asserting on the
bytes the document holds passes either way, since `add_asset` already dedupes
identical content. Memory never showed this; the I/O is the bug, so the test
counts reads.

Audit DC-14. Its second half -- capping the pages a merge produces -- is not
here: how many pages a merge may make is a product decision, and the records
are the user's own data rather than something arriving from outside.
Found while testing crates/idml/src/export/designmap.rs:208-230 (xref_format_el)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes #1881

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Fixes #1883

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`len` and `is_empty` were added for a test that ended up counting reads
through `io::touched` instead, so they were dead code -- and the gate runs
`cargo clippy --workspace --all-targets -- -D warnings`, which turns
`dead_code` into a failure. Hosted CI was red on this PR.

My local check had been `cargo clippy -p designcraft-engine --all-targets`
without `-D warnings`, which is why I missed it; re-ran it the way the gate
does.

Found by the review bot, confirmed against the CI log.
A story whose custom or above-line anchored objects wrap its own later
lines is composed first without those wraps, then again with each
object where the previous pass placed it. That loop stopped after a
fixed MAX_PASSES = 4 extra compositions. Each object's wrap pushes down
the anchors of the objects after it, so in a chain of such objects the
k-th object only lands where it belongs once the k - 1 wraps before it
are in place. From roughly the fifth chained object on, the loop ran
out before the story settled: the final composition placed the object
in one spot but wrapped the text around where an earlier pass had put
it, and lines printed straight over the object.

Bound the loop by the story's own wrapping objects instead: n + 1
passes for n such objects (inline objects never wrap their own story
and are left out). Composition 0 has no own wraps, so object 1 is
already where it belongs; by induction pass k places object k + 1
correctly, pass n places the last one, and pass n + 1 wraps the text
around it, after which the positions cannot move. The existing settled
check still stops a story early as soon as its objects stop moving,
so stories that settle sooner cost nothing extra. MAX_PASSES stays as
an absolute safety net, raised to 64, for layouts that keep moving
their objects (keep options, vertical justification) and for stories
with very many anchored objects.

Test: chained_anchored_objects_all_wrap_their_own_storys_lines anchors
eight 100 x 60 pt bounding-box objects six lines apart in one story,
each hanging from its anchor line, and checks that every object sits
on its anchor line and every line it hangs over moves clear of it.
Without this change it fails at the seventh object (a line 790 pt down
sits at x 0 inside the object's band at 718 pt); with it, it passes.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Review caught a wrong assertion in my own test. `\\?\C:\x` is the
extended-length spelling of `C:\x`: craft-docpath parses it as
Root::VerbatimDrive, is_absolute, and its own suite classifies it
PathKind::Local. The test asserted it was refused, so it would have
failed on Windows -- the one platform it runs on.

A data file can already name C:\secret\x.png, so refusing the long
spelling would have shut out long paths without shutting anything out.
It is now asserted the other way, and the refusal list keeps a device
path in its place.

Each shape was checked against the pinned craft-docpath through
resolve(), which recognises Windows forms on every platform, so the
verdicts are first-hand and not read off the source: Network for the
extended UNC prefix, Device for \\.\pipe, GLOBALROOT, CON and NUL.png,
and Ok for \\?\C:\secret\x.png.
`Table::new` allocates `rows * cols` cells up front, and the counts reach it
from IDML and DOCX, where they are whatever the file claims. Measured here:
`size_of::<Cell>()` is 1,360 bytes, so the reported IDML case -- one `<Table>`
with 5,000 `<Row/>` and 5,000 `<Column/>`, about 1 KB zipped -- asks for 25M
cells, some 34 GB. That allocation does not fail politely: it aborts the
process through `handle_alloc_error`, which no `catch_unwind` can catch, so
every open document's unsaved work goes with it.

A table is clamped to InDesign's own limits, 10,000 rows and 1,000 columns,
and to 250,000 cells whatever its shape -- the row and column limits alone
would still allow 13.6 GB. Columns are clamped first and rows take what is
left, because a table is far more often long than wide.

The IDML importer walks the clamped slices and reports the difference.
Indexing its rows and columns by the file's own counts would be out of
bounds, which is how a 34 GB abort would otherwise have become a panic.

Rebased onto the post-split main: the constants and `clamped_size` live in
`doc`'s `table` module root, `Table::new` in `table/grid.rs`, and the IDML
table reader in `import/table.rs`.
Review pointed out that capping each table leaves a file free to hold
many capped tables: three at the per-table limit claim 750,000 cells,
about 1 GB of Cell, with every table inside its own limit. The package
reader's limits count input bytes, which say nothing about how many
cells a row and column count claim.

budgeted_size clamps each table against what the import has left, and
the cells are reserved before Table::new so a cell's own story cannot
spend the budget the table around it is counting on. The budget is
400,000 cells: a Cell carries a whole story and is 1,360 bytes, so that
is about the 512 MiB one part of the file may already decompress to,
and the same count the spreadsheet importer collects at most.

A table is never read as nothing. One row and one column are kept, so
the table stays in the document and still says where its text went, and
a file pays one cell for a table it really does contain -- which its own
size already bounds. The warning says which limit cut the table down.

The arithmetic is tested rather than the allocation: 400,000 cells is
544 MB and three such tables are not a thing to ask CI to allocate. The
sequence test runs 32 claimed sizes, including usize::MAX, and holds the
invariant across all of them.
A footnote reference clones its note and walks it again, and an XLSX shared
string is copied per cell that uses it, so one 1 MB footnote referenced 100k
times meant around 100 GB. The text is kept at the first reference; later ones
get the mark and an empty note, and the report says so. The spreadsheet reader
stops at 64 MB of text and 400,000 collected cells.

Carries both review fixes:

- a footnote id is recorded only once its text has actually been kept, so a
  first reference arriving after the budget was spent no longer counts as
  "done" and tell the next reference the text is at a reference that has none;
- the report and the note budget are both bounded. The duplicate warning names
  the id, so 400 ids referenced twice made 400 warnings, each pushed after a
  Vec::contains scan of all the earlier ones; warnings go through warn() now,
  which dedups through a set and stops at 100 distinct messages. And the budget
  counted only `text`, which holds a table's anchor and not its cells, so 18
  notes of 600 KB of cell text retained 10,800,072 bytes against the 8 MB
  budget; note_bytes() walks the tables and their nested tables.

Rebased onto the post-split main: the implementation stays in docx.rs and the
tests moved to docx/tests.rs, so the test additions went there.
A size that was given but is not one a table can have was corrected in
silence: {"rows": 0} made a table with one row, -1 and 1.5 made the default
four, and 10000 was capped at 500 -- so a script asking for a shape the
command cannot honour got a different table and no indication of it.

A size that is absent still takes the default; a size that is present and
outside the range is refused, naming the key and the range.

Rebased onto the post-split main: `table.insert` moved to
table/structure.rs and the tests to table/sizing.rs. The helper is named
`given` rather than `count`, which that module already imports from
table/target.rs for the repeat count of a row or column insert.
A PDF export recorded the second it was made in, so exporting one document
twice gave different files although the pages matched -- and the instance id
krilla derives from the bytes followed the timestamp, so nothing about the two
files was comparable.

SOURCE_DATE_EPOCH stands in for the clock when the export is given no date, as
the reproducible-builds specification defines it. A date passed in still wins,
and a value the PDF date format cannot hold -- malformed, negative, or past
the range -- is ignored rather than refusing the export or writing a date no
reader can parse. With the epoch pinned the two files are identical byte for
byte.

Rebased onto the post-split main: export_time and pinned_or live in
export/metadata.rs beside now_unix, which they stand in front of, and the call
site is in export/pipeline.rs. The ROADMAP keeps upstream's 2026-10-17 entry
with this one above it.
Found while testing crates/textimport/src/lib.rs:140 (plain) and crates/engine/src/cmd/place_text.rs:372 (place_text)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Enforce Rust coding standards via Clippy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/textimport/src/tagged.rs:~190 (the `n.starts_with("0x")` branch of import: `u32::from_str_radix(&n[2..], 16)`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
The running-header tests move to cmd/variables/tests.rs with upstream's split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
The PDF change moves to export/walk.rs (draw_leaf_content passes the drawn flag back) and the
render test to render/src/tests.rs, with upstream's splits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
* 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.
* 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
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.
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.
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>
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>
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.
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.
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>
Keeping a 1 x 1 table for every further `<Table>` was unbounded in the one
direction the budget exists to close. `<Table></Table>` is fifteen bytes, so
the 512 MiB a part may decompress to holds some 36 million of them, and a
`Cell` is 1,360 bytes: tens of gigabytes, with every table inside both caps.
The test I wrote said as much without my noticing -- its invariant was
`reserved <= MAX + tables`, and `tables` had no bound.

`budgeted_size` now returns `None` once the budget is spent and the table is
not read at all, so the cells one import reserves are at most the budget,
exactly. The invariant is now `reserved <= MAX_IMPORT_TABLE_CELLS`, with no
term that grows with the file.

The warnings needed the same treatment, for the same reason: a file of 400,000
tables each claiming one row and two columns got a line about every one of
them. At most 20 are named, then one line saying the rest were the same, and
the file is told once that its tables ran out of cells between them.
Found while testing crates/textimport/src/tagged.rs:~190-205 (the ParaStyle and CharStyle branches of import: `unescape_value(value.trim())`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/color/src/swatch.rs:94-101 (value_name)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* packaging: Flatpak finds and prints to CUPS printers

The Print dialog listed no printers in the Flatpak: the runtime has libcups but not
lpstat/lp, the sandbox couldn't reach the CUPS socket, and driverless network printers
(lpstat -e) need Avahi. Bundle lpstat/lp/lpoptions from CUPS 2.4.20 (Apache-2.0,
statically linked, licence installed) and grant --socket=cups and
--system-talk-name=org.freedesktop.Avahi in both manifests.

* packaging: Flatpak reads the user's CUPS settings (~/.cups, read-only)

Review (CodeRabbit): without it the sandbox's lp/lpstat ignore the user's default
printer and saved options (lpoptions) and a print server named in their client.conf.

* packaging: Flatpak CUPS tools reach the host socket when client.conf names a custom path

Review: Flatpak mounts the host's CUPS socket at /var/run/cups/cups.sock whatever
path ~/.cups/client.conf or CUPS_SERVER names, so lp/lpstat/lpoptions now run through
a wrapper that points a socket path missing in the sandbox there. Server names
(remote servers) and sockets that exist are left alone. Tested in the sandbox:
CUPS_SERVER=/run/custom/cups.sock lists the host's printers through the wrapper and
none without it; a host name is passed through unchanged.

* packaging: redirect a custom CUPS socket only when it is the one Flatpak mounted

Review (CodeRabbit): Flatpak picks the socket it mounts at /var/run/cups/cups.sock
with stricter rules than CUPS (flatpak-run-cups.c: a socket CUPS_SERVER, else the
first line spelled exactly "ServerName /path" in ~/.cups/client.conf, else the
default), while CUPS reads the keyword in any case and takes the last ServerName. The
wrapper now redirects only when CUPS' socket is the one Flatpak chose, so a job can't
reach a different server; otherwise CUPS fails on the missing path.

* packaging: never redirect a CUPS_SERVER the sandbox was given

Review (CodeRabbit): Flatpak picks the socket it mounts from the host's CUPS_SERVER,
so one set by flatpak run --env or an override may not be what it mounted. The
wrapper now leaves any CUPS_SERVER alone and redirects only a client.conf socket that
matches Flatpak's choice; otherwise CUPS fails on the missing path instead of
reaching a different server.

* packaging: point CUPS at the mounted socket only when it is the one Flatpak chose, else fail

Review (CodeRabbit): with a custom socket listed first and the default last in
~/.cups/client.conf, Flatpak mounts the custom one at /var/run/cups/cups.sock while
CUPS picks the last line, finds that path, and talks to the custom server. The
wrapper now compares CUPS' socket with Flatpak's choice even when the path exists:
equal, CUPS is pointed at the mount; different, the tool fails with an error instead
of reaching another server.
Both are interface languages in LANGUAGES but were missing from the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single wrapping object moved by keep options or vertical justification had four passes to
settle before; one-per-object would have given it two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expectation change is unrelated to hyphenation and lands with #1840.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps add_fields under the cognitive complexity limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repaint the canvas when a placed file is relinked or updated
Fix duplicated xref format text before an unclosed '<' in IDML export
Compose: tables clear the footnote area
Fix "-0" in Name with Color Value for slightly negative RGB
Tables: a shape a table cannot hold is clamped, not allocated (#1583)
Drop XML-forbidden control characters in file.exportXml
i18n(ka): Georgian for keyboard shortcut sets, command forms and labels added since #1573
IDML export: write the document's default StoryPreference
[fix] Running headers read a page in text order, not by height
PDF export: SOURCE_DATE_EPOCH stands in for the clock, so exports repeat
Escape font family and typeface values in Tagged Text export
A table size Create Table cannot mean is refused
Fix RTF import swallowing characters after a short \'hh escape
PDF export: a name escapes every byte a name cannot hold (#1582)
Strip story marker characters from placed plain text files
Data merge: an image named by many records is read once (#1595)
Print: the Windows print test expects the driver's default color
Keep leading and trailing spaces in Tagged Text style names
(fix) macOS: accept .idml dropped on the app icon and opens with contextual "open with..."
Import: a shared string and a footnote are not copied per reference (#1584)
Data merge: the UNC refusal is tested on Windows too (#1601)
macOS: declare the UI languages so the open and save panels are translated
Open documents whose items share an id
compose: settle a story's own anchored wraps by object count, not 4 passes
Add German 2006 reform hyphenation
Rulers: a guide dragged out keeps its resize cursor until it is dropped
Export form field borders from the item stroke
# Conflicts:
#	crates/engine/src/cmd/xml.rs
# Conflicts:
#	crates/textimport/src/tagged.rs
# Conflicts:
#	docs/typography-parity.md
Reject invalid XML tag names in xml.mapStyle
Autoflow: a frame a Start Paragraph option ends early sets no fill rate (fixes #1209)
Skip signed hex codes in Tagged Text <0x…> tags
[fix] Inline objects paint under their frame's text
Compose: honor discretionary hyphens in single-line composition
Story: copy a paragraph's format for all new returns at once (part of #1588)
Effects: inner shadow, glow and bevel follow a transparent image's edge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: keep ROADMAP progress log newest first
AGENTS.md (and CLAUDE.md where present) gets a CI workflows section: workflows only call
craft-common-actions at @main, with no other jobs; feature and fix PRs don't touch CI and
link or open a craft-common-actions issue (a PR only for a small fix that is safe for
every app and does not increase CI time); legacy jobs
aren't added to and move only as their own task, staying until a shared workflow runs them.

windows-arm64.yml moves from its SHA pin to @main. The pinned revision is on
craft-common-actions main and the workflow (and the actions it calls) is identical
there, so nothing changes in behaviour.
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#7bb51c, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/engine/src/cmd/export/text_tests.rs
- crates/pdf/src/gradient_inks.rs
UPD.1: honor app pretend versions in update checks (#2025)
Some checks failed
ci.yml / UPD.1: honor app pretend versions in update checks (#2025) (pull_request) Failing after 0s
nix.yml / UPD.1: honor app pretend versions in update checks (#2025) (pull_request) Failing after 0s
packaging-lint.yml / UPD.1: honor app pretend versions in update checks (#2025) (pull_request) Failing after 0s
sonarcloud.yml / UPD.1: honor app pretend versions in update checks (#2025) (pull_request) Failing after 0s
ci.yml / UPD.1: honor app pretend versions in update checks (#2025) (push) Failing after 0s
nix.yml / UPD.1: honor app pretend versions in update checks (#2025) (push) Failing after 0s
packaging-lint.yml / UPD.1: honor app pretend versions in update checks (#2025) (push) Failing after 0s
sonarcloud.yml / UPD.1: honor app pretend versions in update checks (#2025) (push) Failing after 0s
ea08e4c2ba
alf merged commit ea08e4c2ba into main 2026-10-30 07:58:50 +00:00
alf deleted branch chore/sync-upstream-20261030 2026-10-30 07:58:50 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
iris/designcraft!14
No description provided.