Commit Graph
4115 Commits
Author SHA1 Message Date
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 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
aiongg d36e5fb63f Merge pull request #1927 from Silvertree2010/perf/export-speed
PDF export: ask the composition cache once per story and page
2026-10-27 13:41:05 -04:00
macadminHU 72a586e0d5 HU1: keep the moved PDF test helpers clippy-clean
Simplify two upstream Option predicates to equivalent is_none_or forms. Preserve the None cases, all test cases and assertions; no coverage is removed.
2026-10-27 18:15:37 +01:00
macadminHU e6e8fc126e HU1: keep the integrated PDF condition clippy-clean
Apply the equivalent De Morgan form of the upstream gradient condition so Rust 1.95 clippy with warnings denied can complete. Short-circuit order and output behavior are unchanged.
2026-10-27 18:15:05 +01:00
macadminHU 4207debc17 HU1: retain Hungarian in the moved language command metadata
Keep hu in the app.language parameter documentation after the UI command table moved to menus/ui_commands.rs.
2026-10-27 18:14:01 +01:00
macadminHU f8b05ee3df HU1: retain toolbar coverage with current keymap captions
Preserve existing toolbar assertions and Hungarian right-click flyout coverage in both column layouts. Check platform-formatted default shortcuts and a user-assigned chord, including painted text, clipping and overlap. All 13 focused toolbar tests passed.
2026-10-27 18:11:25 +01:00
macadminHU 39109cc0df HU1: retain both parents menu tests in the shared layout
Retain the upstream shortcut and navigation tests together with Hungarian selection, persistence, menu-path and document-preservation regressions and the complete Georgian-menu assertion.
2026-10-27 18:10:55 +01:00
macadminHU c9255c30ae HU1: preserve localization regressions in the new test module
Retain localization checks from both merge parents, including Hungarian context and catalog coverage, and directly check the repaired Georgian captions. This file matches the locally tested shared-module integration.
2026-10-27 18:08:51 +01:00
macadminHU 4a85d3b61e Merge branch 'main' into codex/hungarian-localization 2026-10-27 18:07:17 +01:00
macadminHU 450789d627 HU1: align the moved panel test path for the module merge
Upstream d77fdf2d moved these tests to crates/ui-egui/src/panel_docking/tests.rs. The tested local merge preserves every test body from both parents there. Remove the obsolete path to unblock GitHub’s web conflict resolver; the remaining module integration follows immediately.
2026-10-27 18:05:17 +01:00
con3code 206dbe8499 Merge remote-tracking branch 'upstream/main' into fix/effects-panel-units
# Conflicts:
#	crates/ui-egui/src/panels/properties.rs
2026-10-28 02:03:29 +09:00
con3code 7e3c7bd9b3 Merge remote-tracking branch 'upstream/main' into fix/type-fields-read-units
# Conflicts:
#	crates/ui-egui/src/panels/properties.rs
#	crates/ui-egui/src/widgets.rs
2026-10-28 01:37:05 +09:00
NetterSilverandClaude Opus 5.5 1db1635a88 Ask the composition cache once per story and page in PDF export
The PDF exporter called Cache::get twice for every text frame it drew.
Each call rebuilds the cache signature of the whole story, which looks up
every frame of the story with Document::find (a linear walk over all
spreads), and the anchored-frame placements walk them again. For a story
threaded through many frames this is O(frames^2 x items) per export.

The document doesn't change during an export, so the exporter now keeps
the composition it got for each (story, page) and reuses it. The cache
still decides what to compose; the output is byte-identical.

Add examples/pdf_export.rs to designcraft-cli: File > Export PDF through
a headless session on the `designcraft-cli perf` stress document (200
pages, 1M characters in 40 threaded stories, 500 placed images). The
stress document generator moves unchanged from src/perf.rs to a small
library module (src/stress.rs) that the binary and the example share.

Ryzen 16 threads, Linux, 12 runs each, interleaved:
  main     median 3485 ms, min 3427 ms
  patched  median 2016 ms, min 1656 ms  (-42 % median, -52 % min)
The exported PDF is byte-identical (12,075,744 bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:47:25 +01:00
aiongg d77fdf2d4c fix(ui): dropdown menus grow back after a search with no matches (#1917)
* fix(ui): font menus grow back after a search with no matches

Every combo box is craft_ui::ComboBox (craft-libs pinned at 62bb0e1d), whose
menu follows its contents' height while open (egui#5225); clippy.toml bans
egui::ComboBox.

The Font menu splits into a header (search field, filters) and a list
(font_menu_header, font_menu_list). Font fields show it in a
craft_ui::combo::Menu and the style dialogs' Font Family combo boxes in
ComboBox::show_ui_with_header, so only the rows scroll and the search field
stays in place. The blank and Parent's Font rows sit in the header, above
the search.

* chore(deps): craft-libs 60d60d48 (dropdown fixes)

* chore(deps): craft-libs 4d7b82f7 (dropdown fixes)

* chore(deps): craft-libs 92d392cf (#387 merged)

* chore(xtask): pdf-deps expects craft-libs 92d392cf
2026-10-27 10:27:32 -04:00
dotCooCoo 9ef2e64f9f Merge main into the column rule extent fix 2026-10-27 07:16:50 -07:00
NetterSilverandClaude Haiku 5.5 516998d449 Export form field borders from the item stroke
Fixes #1881

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 13:33:17 +00:00
hslrmnp27 66447c01ae T.DE1: merge upstream main and resolve update conflicts 2026-10-27 13:33:24 +01:00
Hamza bb49e56627 Fonts: a missing Korean sans face falls back to a sans one (#1692)
Rebuilt on the split `fontdb/`: `KOREAN_SANS_FALLBACKS` and `sans_by_name` sit
in `fontdb/fallback.rs` beside `KOREAN_FALLBACKS`, `cjk_chain` takes the style,
`fallback_for_styled` is on both `ScopedFonts` and `FontDb`, and the tests live
in `fontdb/tests/fallback.rs` where that file's other fallback tests are.

`KOREAN_FALLBACKS` is serif-first (AppleMyungjo, Apple SD Gothic Neo, Batang,
Malgun Gothic...), so on Windows the first installed entry is Batang. A document
whose Korean text asks for faces that are not installed -- KoPub Dotum, KHNP
Han Dotum, BM Dohyeon -- came out in a serif face throughout: the report
measured 5,298 characters of one school document turning from sans to serif.

The font is not installed, so its name is all there is to go on. `sans_by_name`
reads the style out of the family name: Dotum (돋움), Gulim (굴림), Gothic
(고딕), Sans, Headline, Graphic and Dohyeon are sans; Batang (바탕),
Myungjo/Myeongjo (명조), Gungsuh (궁서) and Serif are serif. Serif is tested
first, so "Noto Serif KR" is not caught by the `sans` rule.

A run whose requested face is sans draws its fallback from the sans chain, which
leads with the system faces. If the requested name says neither, the
substitute's own name decides, which leaves today's chain in place.

The tests do not depend on which fonts the machine has: they pin the name
classification and the chain order -- the first sans entry comes before Batang,
and the serif chain still leads with a serif face, so a missing Myungjo is not
turned into a sans one.

`fallback_for` keeps its signature and delegates, so the ruby, kenten and image
callers are unchanged.
2026-10-27 16:30:57 +05:00
NetterSilverandClaude Haiku 5.5 e8c0a19180 Fix duplicated xref format text before an unclosed '<' in IDML export
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>
2026-10-27 11:26:46 +00:00
NetterSilverandClaude Haiku 5.5 e6a14a516a Keep underline, strikethrough, caps and tracking in EPUB/HTML character styles and overrides
Found while testing crates/epub/src/css.rs:~128-153 (the character-style loop in stylesheet_with)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 11:02:53 +00:00
Hamza 7c25cd97c4 Data merge: an image named by many records is read once (#1595)
`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.
2026-10-27 15:25:27 +05:00
Hamza 93ab34f515 Data merge: the UNC refusal is tested on Windows too (#1601)
`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).
2026-10-27 14:16:27 +05:00
NetterSilverandClaude Haiku 5.5 7fcebedf98 Drop XML-forbidden control characters in file.exportXml
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>
2026-10-27 09:07:12 +00:00
NetterSilverandClaude Haiku 5.5 590ea661a4 Fix RTF import swallowing characters after a short \'hh escape
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>
2026-10-27 08:29:34 +00:00