Commit Graph
4827 Commits
Author SHA1 Message Date
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
Claude d257578c71 Merge storytold/main (v0.14.0) into idml-app-icon-drop
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
2026-10-28 18:00:04 +00:00
Dylan M. Taylor 32119f34ee Grant network access so update checks can reach GitHub (#1954) 2026-10-28 13:07:31 -04: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
Brandon ThomasandClaude Opus 5.5 7da69c16ce Release 0.14.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.14.0
2026-10-28 11:52:07 -04:00
Brandon ThomasandClaude Opus 5.5 2ea0dfc11f macOS packaging: retry stapler when Apple's CloudKit drops the connection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:52:02 -04:00
Dylan M. Taylor e46dcd9516 Merge pull request #1899 from dylanmtaylor/sonar/enforce-clippy-standards
Enforce Rust coding standards via Clippy
2026-10-28 11:32:03 -04: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 434e269691 cli: the three argument bugs photocraft#7071 lists are live here too
- An option took the next token whatever it was, so `--export --scale` wrote a
  file called `--scale` in the working directory. A token that cannot be a
  value is refused, and where the value is a path the message offers the
  shell's own way out (`./-name`). A negative number can still be a value, and
  `-inf`/`-nan` still reach the check that reports them.
- An argument that is not UTF-8 panicked inside `std::env::args`. It is a
  usage error now, named, since such a path cannot reach the engine anyway.
- `<sub> --help` was read as an unknown option by every parser but `script`'s.
  It answers from the usage block, on stdout, and nothing else happens.

Rebased onto the post-split main, where the fix had to grow one part. Upstream
has since added a check that an export option sits before its `--export`, and
it consumed the following token whatever it was, so `--scale --export out.png`
was reported as the *order* of the options -- advice to move an option that is
already where it belongs -- when what is wrong is that `--scale` has no value.
That check now skips a value only where the next token can be one, and stands
aside entirely when a value is missing so the value error speaks. Its own test,
`run_export_options_come_before_their_export`, still passes.

The CLI's tests moved to tests/it/cli.rs, so the test additions went there.
2026-10-28 18:34:12 +05: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
Dylan M. Taylor b0d8ae7f89 Enforce workspace Clippy standards and document lint policy 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 251dabb2b5 Refactor designcraft-cli for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor d04c8b2e58 Refactor ui-egui for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 5b21472180 Refactor engine for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 072dd02f73 Refactor render for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor be9b8f360e Refactor idml for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 0104122f06 Refactor doc for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor e6ff9d5aac Refactor fonts for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 71a579e8da Refactor compose for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor 07366af1b6 Refactor pdf for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor bcb9546f5d Refactor tools for strict Clippy checks 2026-10-28 11:39:26 +00:00
Dylan M. Taylor f1a5744e8c Refactor textimport for strict Clippy checks 2026-10-28 11:39:25 +00:00
Dylan M. Taylor c2869280a2 Refactor epub for strict Clippy checks 2026-10-28 11:39:25 +00:00
Hamza 1995fb5feb IDML import: the tables in one file are capped between them
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.
2026-10-28 16:31:34 +05:00
Hamza b49535624c Tables: a shape a table cannot hold is clamped, not allocated (#1583)
`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`.
2026-10-28 16:26:13 +05:00
Hamza 0d16a04038 Fonts: the word "serif" outranks every sans indicator
My own previous commit introduced this: moving the bare "serif" needle
after the sans list to let "Microsoft Sans Serif" read as sans also let
"gothic" win in "Sweet Gothic Serif", a real serif family, which review
caught.

The rule is narrower than either ordering. A name holding "serif" is
serif, and that outranks the sans indicators; the only exception is the
phrase it ends: "sans serif" and "sans-serif" are the name of a sans
face. So the phrase is removed before the word is looked for, which
keeps both spellings sans and gives every explicitly serif name back.

Korean serif names stay ahead of all of it, so "Noto Sans Myungjo" and
"Myungjo Gothic" are unchanged.
2026-10-28 15:53:44 +05:00
Hamza ff35b3561f Data merge: an extended-length drive path is a local file, not a refusal
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.
2026-10-28 15:33:12 +05:00
garrytanandcapy-ai[bot] 4ac4219fd2 compose: settle a story's own anchored wraps by object count, not 4 passes
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>
2026-10-28 10:23:57 +00:00
Hamza 8444abfbca Fonts: "sans serif" is a sans name, not a serif one
Review pointed out that the CSS generic "sans-serif" matched the bare
"serif" needle first and classified as serif. The same ordering hit real
family names: "Microsoft Sans Serif", "MS Sans Serif" and "Lucida Sans
Serif" are sans faces that a Word import can name, and epub's own
generic_family already maps Microsoft Sans Serif to sans-serif.

So rather than special-casing the one hyphenated spelling, the bare
"serif" word now sorts after the sans needles. The Korean serif names
stay ahead of everything, which is what keeps "Noto Sans Myungjo" and
"Myungjo Gothic" serif; the test pins those two along with both "sans
serif" spellings.
2026-10-28 15:18:58 +05:00
Hamza 0c9e59c009 Fonts: do not read a requested style off the face that stands in for it
Review of #1755 caught a real over-reach in my own patch. When a family
cannot be resolved, db.face hands back a bundled face, and I was reading
"Sans" out of its name: FALLBACK_FAMILY is "Source Sans 3", so every
unclassified missing font took the sans chain -- picking Malgun Gothic on
Windows over the Batang fallback that was already there. DEFAULT_FAMILY,
"Source Serif 4", would have forced the other way.

A stand-in face says nothing about what the document asked for, so
style_of_substitute returns None for both and only a face that really
names itself gets a vote. The test pins both families by their constants;
with the old body it fails with Some(true) against None.
2026-10-28 15:15:15 +05:00
ashokram-si 5f5101a791 Canvas tests: the right-to-left spine test uses pages of different heights
Review follow-up: the spine runs the height of the taller page. Make the
narrower right page shorter too, so a line that stopped at the page
listed first would fail the test.
2026-10-28 13:43:34 +05:30
ashokram-si 88e71d0777 Canvas: the spine follows the shared edge of a right-to-left spread
Review follow-up: bound right to left, a spread lists its right page
first, so averaging the first page's right edge with the second page's
left edge put the line halfway between the outer edges when the pages
differ in width. Order the two pages by position and use the edges that
meet. Test: a right-to-left spread whose right page is narrower.
2026-10-28 12:54:59 +05:30
Hamza a30744fed1 Drop the unused ImageCache inspection methods
`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.
2026-10-28 11:53:56 +05:00
ashokram-si c51c8fb49d Canvas: a spine line where the pages of a spread meet (#1838)
In a spread with two or more pages the paper is one rectangle and
nothing marked where the pages meet. Draw a hairline at each shared
page edge, the full height of the taller page, in the guides pass: it
shows on screen only, hides with the other guides and in the preview
modes, and exports never see it since the render crate draws no chrome.

Tests: a three-page facing document gets one spine between pages 2 and
3 and none at outer edges; the line goes with guides off, in Preview
and in Presentation.
2026-10-28 12:19:13 +05:30
NetterSilverandClaude Opus 5.5 8785934032 Open documents whose items share an id
Fixes #1883

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 01:50:17 +00:00
Davit MatchakhelidzeandClaude Opus 5.5 20d6722820 i18n(ka): keep the Letter, Legal and Letter - Half paper sizes in English, like Tabloid
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
2026-10-28 05:00:07 +04:00
Davit MatchakhelidzeandClaude Opus 5.5 40083be3f2 i18n(ka): Georgian for keyboard shortcut sets, command forms and labels added since #1573
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
2026-10-28 04:07:38 +04:00
aiongg 2d8207ed3c Merge pull request #1896 from Jeremy8776/feat/install-scope-policy
Windows installation policy: DesignCraft
2026-10-27 17:16:38 -04:00
Jeremy eb25f8ad96 test(installer): cover the compiled folder searches, resolver chains and wizard folder replay
The policy check now reads the compiled AppSearch and RegLocator rows for the
remembered-folder properties (registry root, key, value name and type for both
scopes) and runs the DIR, FOLDER, PATH and legacy exe actions in sequence order
against seeded values, so their precedence and the preset-folder guard cannot
regress. It also replays the wizard folder actions in sequence order (a
remembered folder beats the new-install default, a typed folder beats both) and
requires the machine restore action between the scope page and costing.
2026-10-27 21:13:27 +00:00
aiongg 6ba87b1b0c ci: use storytold/craft-common-actions at the merged installer-policy commit 2026-10-27 20:51:57 +00:00
Jeremy 0b31b002eb fix(installer): check the exact architecture-specific machine folder
The pre-signing check matched any folder named Program Files, so it rejected
a valid MSI when Windows resolves Program Files elsewhere, accepted the 64-bit
root for an x86 package and accepted a different app name. Build the expected
folder from the package architecture's resolved root and the policy name.
2026-10-27 19:08:34 +00:00
aiongg 1abeda4459 Merge pull request #1802 from dotCooCoo/fix/column-rules-span-the-text
Text frames: column rules run from the top line's ascent to the bottom line's descent
2026-10-27 13:45:19 -04:00
aiongg 5d8739b3d8 Merge pull request #1795 from dotCooCoo/feat/story-editor-depth-ruler
Story Editor: a depth ruler beside the text, and View › Story Editor › Show/Hide Depth Ruler, as in InDesign
2026-10-27 13:44:46 -04:00