Commit Graph
4115 Commits
Author SHA1 Message Date
moppeeer 740227bd18 Merge remote-tracking branch 'origin/main' into snap-page-centre
# Conflicts:
#	crates/tools/src/snap.rs
2026-10-29 11:43:39 +01:00
moppeeer b26ae037de Merge remote-tracking branch 'origin/main' into corner-options-dialog
# Conflicts:
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/panels/properties.rs
2026-10-29 11:43:35 +01:00
moppeeer 204da7bd75 Merge remote-tracking branch 'origin/main' into create-guides-spread
# Conflicts:
#	crates/ui-egui/src/dialogs.rs
2026-10-29 11:43:32 +01:00
moppeeer c393c7fbd8 Merge remote-tracking branch 'origin/main' into object-menu-order
# Conflicts:
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/menus.rs
2026-10-29 11:43:29 +01:00
moppeeer 00a64e5943 Merge remote-tracking branch 'origin/main' into direct-select-anchor-points
# Conflicts:
#	crates/tools/src/select.rs
#	crates/ui-egui/src/canvas.rs
2026-10-29 11:43:21 +01:00
moppeeer 542ee6d5bc Merge remote-tracking branch 'origin/main' into graphic-frame-x-corners
# Conflicts:
#	crates/ui-egui/src/canvas.rs
2026-10-29 11:43:03 +01:00
moppeeer b8edea5692 Merge remote-tracking branch 'origin/main' into drawing-feedback
# Conflicts:
#	crates/ui-egui/src/canvas.rs
2026-10-29 11:42:59 +01:00
moppeeer 5a4012bab8 Merge remote-tracking branch 'origin/main' into properties-anchor-points
# Conflicts:
#	crates/tools/src/select.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/panels/properties.rs
#	docs/target-app-parity.md
2026-10-29 11:42:56 +01:00
moppeeer e094562ef6 Merge remote-tracking branch 'origin/main' into clear-fitting-options
# Conflicts:
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/menus.rs
2026-10-29 11:42:45 +01:00
moppeeer b6fec896f9 Merge remote-tracking branch 'origin/main' into effect-blend-modes
# Conflicts:
#	crates/doc/src/item.rs
#	crates/idml/src/export.rs
#	crates/idml/src/import.rs
#	crates/ui-egui/src/effects_dialog.rs
2026-10-29 11:42:14 +01:00
moppeeer 4145fcbc44 Merge remote-tracking branch 'origin/main' into text-frame-options-layout
# Conflicts:
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/i18n.rs
#	docs/target-app-parity.md
2026-10-29 11:41:36 +01:00
moppeeer d5b9771937 Merge remote-tracking branch 'origin/main' into properties-bullets-options
# Conflicts:
#	crates/ui-egui/src/panels/properties.rs
2026-10-29 11:41:11 +01:00
moppeeer 1fee4511af Merge remote-tracking branch 'origin/main' into properties-fill-color-picker
# Conflicts:
#	crates/ui-egui/src/panels/properties.rs
2026-10-29 11:40:27 +01:00
moppeeer d25577fefe Merge remote-tracking branch 'origin/main' into swatch-inline-rename
# Conflicts:
#	crates/ui-egui/src/panels/swatches.rs
2026-10-29 11:40:25 +01:00
moppeeer e7cb2ba0c4 Merge remote-tracking branch 'origin/main' into color-panel-gradient-stop
# Conflicts:
#	crates/ui-egui/src/panels/swatches.rs
2026-10-29 11:40:16 +01:00
moppeeer c73f68bccf Merge remote-tracking branch 'origin/main' into status-bar-parent-page-box
# Conflicts:
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/chrome.rs
#	crates/ui-egui/src/panels/properties.rs
2026-10-29 11:40:10 +01:00
moppeeer 6b57bebd6b Merge remote-tracking branch 'origin/main' into insert-pages-dialog
# Conflicts:
#	crates/engine/src/cmd/layout.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/i18n.rs
#	crates/ui-egui/src/menus.rs
#	crates/ui-egui/src/panels/pages.rs
2026-10-29 11:40:06 +01:00
moppeeer c5ac590cbf Merge remote-tracking branch 'origin/main' into recent-items-whole-number
# Conflicts:
#	crates/ui-egui/src/dialogs.rs
2026-10-29 11:39:55 +01:00
moppeeer 88010aa328 Merge remote-tracking branch 'origin/main' into paragraph-border-links
# Conflicts:
#	crates/ui-egui/src/dialogs.rs
2026-10-29 11:39:27 +01:00
moppeeer fffafb7122 Merge remote-tracking branch 'origin/main' into global-units-preferences
# Conflicts:
#	crates/engine/src/cmd/file.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/menus.rs
2026-10-29 11:39:22 +01:00
moppeeer 45751393ef Merge remote-tracking branch 'origin/main' into hyphenation-french-italian 2026-10-29 11:39:17 +01:00
moppeeer f0b580074d Merge remote-tracking branch 'origin/main' into direct-select-handles
# Conflicts:
#	crates/tools/src/select.rs
#	crates/ui-egui/src/canvas.rs
2026-10-29 11:38:53 +01:00
moppeeer 0d9b7c74ac Merge remote-tracking branch 'origin/main' into table-cell-keys
# Conflicts:
#	crates/engine/src/cmd/table.rs
#	crates/engine/src/cmd/text.rs
2026-10-29 11:38:49 +01:00
moppeeer c0af54eabd Merge remote-tracking branch 'origin/main' into table-hidden-chars
# Conflicts:
#	crates/ui-egui/src/canvas.rs
2026-10-29 11:37:40 +01:00
Hamza 5a53a3807c Check a parent page's frame on the pages that show it
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.
2026-10-29 14:17:21 +05:00
NetterSilverandClaude Opus 5.5 4d88c0899e Keep stroke styles from joining the subpaths of a compound path
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>
2026-10-29 09:21:20 +01:00
KnottyDyesandClaude Opus 5.5 d7bc6633e7 Compose: honor discretionary hyphens in single-line composition
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>
2026-10-29 00:14:44 -06:00
VoiceLessQ 033fc2e17e PDF: count recompress decodes in the oversized-image test (#1586)
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.
2026-10-29 00:12:33 -02:00
VoiceLessQ 30a0f08698 Story: copy a paragraph's format for all new returns at once (#1588)
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.
2026-10-29 00:08:22 -02:00
VoiceLessQ d5f07d30eb PDF: check the RGBA budget before recompressing an image (#1586)
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.
2026-10-28 23:28:11 -02:00
VoiceLessQ 89ccfbb18c Autoflow: count each forced move once, and only visible breaks (#1209)
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.
2026-10-28 23:08:41 -02:00
Henrique CabralandClaude Opus 5.5 50b5936b78 IDML export: write the document's default StoryPreference
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>
2026-10-28 21:07:42 -04:00
VoiceLessQ 5eddc821ec Images: refuse a raster that would decode past 1 GiB of RGBA (fixes #1586)
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.
2026-10-28 22:50:39 -02:00
KnottyDyesandClaude Opus 5.5 ed88f3e4b5 Compose: tables clear the footnote area
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>
2026-10-28 18:44:34 -06:00
VoiceLessQ ca85be89d2 Autoflow: skip deleted paragraphs and ask for every forced frame at once (#1209)
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.
2026-10-28 22:32:07 -02:00
VoiceLessQ e248b63165 Autoflow: a frame a Start Paragraph option ends early sets no fill rate (fixes #1209)
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.
2026-10-28 21:46:56 -02:00
VoiceLessQ ba1ee2329b Give anchored objects fresh ids when copying text frames (fixes #1522)
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.
2026-10-28 21:10:50 -02:00
aiongg 855d606325 Show file and message dialogs without blocking the frame (fixes macOS crash) (#1961)
* 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
2026-10-28 17:13:45 -04:00
Claude 97b42e3476 Cross-references: the edited text's IDML export moves into its own method
Keeps story_para under clippy's cognitive-complexity limit, which upstream now enforces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 19:03:09 +00:00
Claude 11f4364705 Merge storytold/main (v0.14.0) into xref-edited-text
The IDML import and export of edited cross-reference text move to import/story.rs and
export/story.rs with upstream's split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 18:09:43 +00:00
Claude 5c6909db46 Merge storytold/main (v0.14.0) into inline-objects-under-text
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
2026-10-28 18:03:03 +00:00
Claude 401928d0fc Merge storytold/main (v0.14.0) into parent-page-display
The canvas fix moves to canvas/frames.rs and its test to canvas/tests.rs; the variables test
lines to cmd/variables/tests.rs, with upstream's splits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 18:01:54 +00:00
Claude 94e0e75249 Merge storytold/main (v0.14.0) into running-header-order
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
2026-10-28 18:00:57 +00:00
NetterSilverandClaude Haiku 5.5 ae7e87ddd0 Skip signed hex codes in Tagged Text <0x…> tags
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>
2026-10-28 17:22:28 +01:00
NetterSilverandClaude Haiku 5.5 79b871b94f Strip story marker characters from placed plain text files
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>
2026-10-28 15:18:35 +00:00
Hamza 87b49d9973 PDF export: SOURCE_DATE_EPOCH stands in for the clock, so exports repeat
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.
2026-10-28 19:09:25 +05:00
Hamza 2f403698e0 An export says what text its pages had no room for (#1465)
A page-based export leaves out the text its frames cannot fit, and said
nothing about it: an imported 58-page form lost whole sentences -- a
data-consent clause, a statutory rule, the last row of two tables -- with
`warnings` empty while preflight reported 19 overset stories.

The PDF and deck exports now report it, in the words preflight uses and with
the same count, naming the page and the frame. A reflowable export carries the
whole story, so it has nothing to report.

Carries the review fix: a spread export puts the whole spread on the sheet, so
choosing page 2 of a facing-pages document exports page 3 as well and page 3's
overset is as absent from the file as page 2's would be. The selection is
widened to the spreads it touches first. A page on another spread is still not
reported, so the filter keeps doing what it is for.

Rebased onto the post-split main: the PDF command is in export/pdf.rs, the
deck one in export/formats.rs, and preflight.rs is unchanged by the split. The
helper sits before export_pdf rather than between first_page_ref's doc comment
and its function, which is where the pre-split patch had put it.
2026-10-28 18:53:45 +05:00
hslrmnp27 0ecaa95206 T.DE1: merge upstream main while preserving German hyphenation 2026-10-28 14:52:15 +01:00
Hamza 352609117f A table size Create Table cannot mean is refused
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.
2026-10-28 18:20:13 +05:00
Hamza 96b498ca70 Import: a shared string and a footnote are not copied per reference (#1584)
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.
2026-10-28 18:10:41 +05:00