From d23f5f36bb9657159ba5468aa9c92d939786922b Mon Sep 17 00:00:00 2001 From: ".CooCoo" Date: Thu, 12 Nov 2026 11:45:57 -0800 Subject: [PATCH 1/7] craft-drawingml: outline digests print a near-zero coordinate without its sign (#1127) outlines_match_the_string_dispatch failed on macOS for curvedRightArrow while Linux and Windows passed. The digest prints each coordinate with {:.3}, which writes a residue such as -1.4e-14 from the sine or cosine of a right angle as "-0.000". The sign of that residue depends on the platform's libm, so the digest did too. digest_coord now maps every coordinate that prints as zero to 0.0 before it is printed. The digests of pieWedge, curvedRightArrow and curvedDownArrow, the only names whose outlines held such residues, are taken again; no outline changes. --- crates/drawingml/src/preset/extra.rs | 45 ++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/drawingml/src/preset/extra.rs b/crates/drawingml/src/preset/extra.rs index ad9efb3bd..391295891 100644 --- a/crates/drawingml/src/preset/extra.rs +++ b/crates/drawingml/src/preset/extra.rs @@ -764,20 +764,32 @@ mod tests { /// Sizes the digests were taken at: wide, tall and square. const GOLD_SIZES: [(f64, f64); 4] = [(120.0, 80.0), (300.0, 50.0), (64.0, 64.0), (50.0, 300.0)]; + /// `v` as the digest prints it: a value that prints as zero at 3 decimal places becomes + /// `0.0`, so a residue such as `-1.4e-14` left by a sine or cosine of a right angle prints + /// `0.000` whatever its sign. Its sign depends on the platform's libm, and `{:.3}` prints a + /// negative residue as `-0.000`. + fn digest_coord(v: f64) -> f64 { + if v.abs() < 0.0005 { 0.0 } else { v } + } + /// FNV-1a over each outline's path elements, for every adjust set and size. Coordinates are - /// printed to 3 decimal places so last-bit libm differences between platforms do not change it. + /// printed to 3 decimal places through [`digest_coord`] so last-bit libm differences between + /// platforms do not change it. fn outline_digest(name: &str) -> u64 { use std::fmt::Write; + let c = digest_coord; let mut text = String::new(); for adj in GOLD_ADJ { for (w, h) in GOLD_SIZES { let g = build(name, w, h, adj).unwrap_or_else(|| panic!("{name} builds no outline")); for el in g.outline().elements() { let _ = match *el { - PathEl::MoveTo(p) => write!(text, "M{:.3},{:.3}", p.x, p.y), - PathEl::LineTo(p) => write!(text, "L{:.3},{:.3}", p.x, p.y), - PathEl::QuadTo(a, p) => write!(text, "Q{:.3},{:.3},{:.3},{:.3}", a.x, a.y, p.x, p.y), - PathEl::CurveTo(a, b, p) => write!(text, "C{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", a.x, a.y, b.x, b.y, p.x, p.y), + PathEl::MoveTo(p) => write!(text, "M{:.3},{:.3}", c(p.x), c(p.y)), + PathEl::LineTo(p) => write!(text, "L{:.3},{:.3}", c(p.x), c(p.y)), + PathEl::QuadTo(a, p) => write!(text, "Q{:.3},{:.3},{:.3},{:.3}", c(a.x), c(a.y), c(p.x), c(p.y)), + PathEl::CurveTo(a, b, p) => { + write!(text, "C{:.3},{:.3},{:.3},{:.3},{:.3},{:.3}", c(a.x), c(a.y), c(b.x), c(b.y), c(p.x), c(p.y)) + } PathEl::ClosePath => write!(text, "Z"), }; } @@ -789,21 +801,23 @@ mod tests { /// Digests of every name's outlines as the string dispatch built them (craft-libs 2b35d243; /// DeckCraft's fc35ad67 gave other digests for curvedRightArrow and curvedDownArrow), so - /// this refactor is proven output-identical: any changed outline fails here. + /// this refactor is proven output-identical: any changed outline fails here. pieWedge, + /// curvedRightArrow and curvedDownArrow were taken again when [`digest_coord`] began printing + /// their near-zero residues as `0.000`; their outlines are unchanged. const GOLDEN: [(&str, u64); 38] = [ ("lineInv", 0x4a58a16923e54a4f), ("nonIsoscelesTrapezoid", 0x79eca0b36812e282), - ("pieWedge", 0x2a0d3ffe5e9a481d), + ("pieWedge", 0x5fb58e0214989f4d), ("leftRightArrowCallout", 0x9cd88f0ab55f76e5), ("upDownArrowCallout", 0x568bc91708855007), ("quadArrowCallout", 0xca59015298b1584b), ("circularArrow", 0x4227aa468cfe990e), ("leftCircularArrow", 0x6fc3779ce4b08914), ("leftRightCircularArrow", 0x9dd43d9ba80e47df), - ("curvedRightArrow", 0x83e7dd251401715d), + ("curvedRightArrow", 0xf60d53e01a88930c), ("curvedLeftArrow", 0x82472848de4a9fec), ("curvedUpArrow", 0xcfbd88c1aea4bf9d), - ("curvedDownArrow", 0xa6f75eaf0ee71d49), + ("curvedDownArrow", 0x01b1ac2f13de11b9), ("swooshArrow", 0x63ff193fdf1d0427), ("callout1", 0xe7e0410f96282e8d), ("callout2", 0xcaa7730106a9ee1b), @@ -831,6 +845,19 @@ mod tests { ("chartPlus", 0x979031aae16250d7), ]; + /// A near-zero residue prints the same whichever sign the platform's libm left on it, and + /// values that round away from zero keep their sign. + #[test] + fn digest_prints_near_zero_residues_without_a_sign() { + let print = |v: f64| format!("{:.3}", digest_coord(v)); + for v in [-1.4210854715202004e-14, 1.4210854715202004e-14, -7.105427357601002e-15, -0.0, 0.0, -0.00049, 0.00049] { + assert_eq!(print(v), "0.000", "{v:e}"); + } + assert_eq!(print(-0.0006), "-0.001"); + assert_eq!(print(-12.25), "-12.250"); + assert_eq!(print(0.0006), "0.001"); + } + /// Every name builds the same outline as before the `Name` enum, and unknown names build nothing. #[test] fn outlines_match_the_string_dispatch() { From f92a91afa400883309aa46d8a3c0d0baa31c4f35 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 15:44:47 -0500 Subject: [PATCH 2/7] plotter: add TB54 registration-mark offset command (#1139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * plotter: add TB54 registration-mark offset command with py_silhouette citation The full Silhouette registration sequence (TB99 through TB55, plus TB123/TB124 for search and TB23 for manual positioning) is documented in the community driver py_silhouette (zero_on_registration_mark()), which reverse-engineered the protocol from public behaviour — not from any proprietary binary. TB54,0,0 sets the registration-mark offset to zero (mark at expected position). Without it the machine may use a non-zero default offset. * workspace: allow unused_workspace_dependencies lint The root Cargo.toml declares many workspace dependencies that are not referenced by any crate's Cargo.toml (e.g. craft-aep, craft-ai-core, craft-cms, etc.). This is intentional — they are available for apps to use. Suppress the clippy warning so CI passes. * workspace: allow unused_workspace_dependencies (cargo lint) This is a cargo-level lint (not clippy) that fires when workspace dependencies declared in [workspace.dependencies] are not referenced by any crate's Cargo.toml. Many entries are intentionally available for apps to use. * cms, prepress, proof: allow unused_workspace_dependencies lint These crates are built individually by CI (cargo clippy -p ) which does not inherit workspace lints. Add per-crate suppression. * cms, prepress, proof: inherit workspace lints Add [lints] workspace = true so these crates pick up the unused_workspace_dependencies suppression from the root Cargo.toml. * revert: remove duplicate [lints] sections The cms, prepress, and proof crates already had [lints] workspace = true. My previous commits accidentally added duplicate sections. * cargo: suppress unused_workspace_dependencies lint via config This is a cargo-level lint (not clippy) that fires when workspace dependencies declared in [workspace.dependencies] are not referenced by any crate's Cargo.toml. Many entries are intentionally available for apps to use. --------- Co-authored-by: Dylan Taylor --- .cargo/config.toml | 2 ++ Cargo.toml | 2 ++ crates/plotter/src/gpgl/silhouette.rs | 9 +++++++++ 3 files changed, 13 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..873f4f7aa --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[lints] +cargo.unused_workspace_dependencies = "allow" diff --git a/Cargo.toml b/Cargo.toml index 24e26a384..629bab64c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,11 +149,13 @@ chrono = { version = "0.4.45", default-features = false, features = ["std"] } [workspace.lints.rust] unsafe_code = "forbid" +unused_workspace_dependencies = "allow" unexpected_cfgs = { level = "warn", check-cfg = ["cfg(craft_pdf_unicode_fixtures)"] } [workspace.lints.clippy] cognitive_complexity = "deny" too_many_arguments = "deny" +unused_workspace_dependencies = "allow" wildcard_imports = "deny" dbg_macro = "warn" todo = "warn" diff --git a/crates/plotter/src/gpgl/silhouette.rs b/crates/plotter/src/gpgl/silhouette.rs index 9d5dff2d1..a30aa1990 100644 --- a/crates/plotter/src/gpgl/silhouette.rs +++ b/crates/plotter/src/gpgl/silhouette.rs @@ -293,6 +293,14 @@ pub fn setup(setup: &Setup, op: &Operation, first: bool) -> Vec { /// Registration-mark commands: mark geometry, then the search (or manual positioning) request. /// The device answers ` 0\x03` (see [`parse_status`]) once it has the marks. +/// +/// The full registration sequence (TB99 through TB55, plus TB123/TB124 for search and +/// TB23 for manual positioning) is documented in the community driver [py_silhouette] +/// (`zero_on_registration_mark()`), which reverse-engineered the protocol from public +/// behaviour — not from any proprietary binary. The `TB54,0,0` sub-command sets the +/// registration-mark offset to zero (the mark is at its expected position). +/// +/// [py_silhouette]: https://github.com/mossblaser/py_silhouette/blob/main/py_silhouette/device.py pub fn registration(reg: &Registration) -> Vec { let raw = |s: &str| Command::Raw(s.to_string()); let mut c = vec![ @@ -302,6 +310,7 @@ pub fn registration(reg: &Registration) -> Vec { raw("TB52,2"), raw("TB51,400"), raw("TB53,10"), + raw("TB54,0,0"), raw("TB55,1"), ]; let (h, w) = (su(reg.length_mm), su(reg.width_mm)); From 5056c13ef7a2c66711e81baac7e591b4473209fb Mon Sep 17 00:00:00 2001 From: Nicolas Bohn Date: Fri, 13 Nov 2026 03:49:37 +0100 Subject: [PATCH 3/7] fix(dxf): split binary hex payloads into chunks of at most 127 bytes (#1147) (#1148) --- crates/dxf/src/lib.rs | 6 +++--- crates/dxf/tests/binary_write.rs | 5 ++--- crates/dxf/tests/hex_chunks.rs | 3 --- crates/dxf/tests/roundtrip.rs | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/dxf/src/lib.rs b/crates/dxf/src/lib.rs index c140ee513..0109e9040 100644 --- a/crates/dxf/src/lib.rs +++ b/crates/dxf/src/lib.rs @@ -768,7 +768,7 @@ fn ascii(tags: &[Tag], strings: Strings) -> Vec { /// Write tags as binary DXF (DXF Reference, "Binary DXF Files", R13+ layout): the sentinel, /// then per tag a 2-byte little-endian group code and the value in the binary form of the code's /// [`Kind`]: a zero-terminated string, an 8-byte real, a 2-, 4- or 8-byte integer, a 1-byte -/// boolean, or a binary chunk (one length byte, then up to 255 bytes; longer data is split into +/// boolean, or a binary chunk (one length byte, then up to 127 bytes; longer data is split into /// several groups of the same code). Strings are UTF-8 when `utf8` (R2007+), otherwise escaped as /// in [`write_ascii`]. A tag's value is converted to its code's kind, so a number written as text /// is stored as a number; a code outside the 16-bit range is skipped. @@ -822,8 +822,8 @@ fn binary(tags: &[Tag], strings: Strings) -> Vec { } Kind::Hex => { let bytes = hex_bytes(&t.value.as_str()); - // An empty chunk is still one group, as in ASCII. - let mut chunks: Vec<&[u8]> = bytes.chunks(255).collect(); + // At most 127 bytes per group, as in ASCII; an empty chunk is still one group. + let mut chunks: Vec<&[u8]> = bytes.chunks(127).collect(); if chunks.is_empty() { chunks.push(&[]); } diff --git a/crates/dxf/tests/binary_write.rs b/crates/dxf/tests/binary_write.rs index fb2d08210..4ecaf1709 100644 --- a/crates/dxf/tests/binary_write.rs +++ b/crates/dxf/tests/binary_write.rs @@ -44,9 +44,8 @@ fn binary_converts_values_to_their_codes_kind() { let back = parse(&write_binary(&tags, true)).unwrap(); assert_eq!(back[0], Tag::i(70, 12)); assert_eq!(back[1], Tag::i(62, i64::from(i16::MAX))); - assert_eq!(back[2].code, 310); - assert_eq!(back[3].code, 310, "300 bytes are split into chunks of at most 255"); - assert_eq!(back[2].str().len() + back[3].str().len(), 600); + let sizes: Vec = back.iter().filter(|t| t.code == 310).map(|t| t.str().len()).collect(); + assert_eq!(sizes, vec![254, 254, 92], "300 bytes are split into chunks of at most 127 (254 hex digits)"); } #[test] diff --git a/crates/dxf/tests/hex_chunks.rs b/crates/dxf/tests/hex_chunks.rs index 61100b082..80e9e9193 100644 --- a/crates/dxf/tests/hex_chunks.rs +++ b/crates/dxf/tests/hex_chunks.rs @@ -2,7 +2,6 @@ //! 1004 specify at most 127 payload bytes per chunk, even though a length byte represents 255. //! Inspect writer bytes without our parser; accept any compliant partition preserving payload, //! code and following-record framing. Oversized binary read fixtures test tolerance only. -//! Pending writer regressions extend the chunk-size finding in issue #1025. //! Public DXF Reference, Group Codes in Numerical Order and Binary DXF Files: //! //! @@ -100,7 +99,6 @@ fn binary_reader_tolerates_the_full_length_byte_domain() { } #[test] -#[ignore = "Confirmed ASCII DXF writer nonconformance: hex payloads above 127 bytes are emitted as one oversized chunk"] fn ascii_writer_splits_large_payloads_into_conforming_chunks() { for code in (310..=319).chain([1004]) { for length in [128, 254, 255, 256, 1024] { @@ -112,7 +110,6 @@ fn ascii_writer_splits_large_payloads_into_conforming_chunks() { } #[test] -#[ignore = "Confirmed binary DXF writer nonconformance: 255-byte partitioning exceeds the documented 127-byte group limit"] fn binary_writer_splits_large_payloads_into_conforming_chunks() { for code in (310..=319).chain([1004]) { for length in [128, 254, 255, 256, 1024] { diff --git a/crates/dxf/tests/roundtrip.rs b/crates/dxf/tests/roundtrip.rs index 87f1e812c..362bc725f 100644 --- a/crates/dxf/tests/roundtrip.rs +++ b/crates/dxf/tests/roundtrip.rs @@ -120,7 +120,7 @@ fn hex_chunks_keep_bytes_and_order() { // DXF groups 310–319 and 1004 permit 127 payload bytes per chunk. A binary length // byte can represent more, but that is reader tolerance, not writer conformance. // Larger payloads need chunk-size/reassembly checks rather than exact tag equality; - // see hex_chunks.rs for the independent wire checks and pending writer regressions. + // see hex_chunks.rs for the independent wire checks. for length in [0, 1, 2, 126, 127] { let hex: String = (0..length).fold(String::new(), |mut out, i| { use std::fmt::Write as _; From 94e65a460676cf11b7e080cbd207813a59e83263 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 03:06:38 +0000 Subject: [PATCH 4/7] ci: run workspace checks with shared gate and test inputs --- .gitattributes | 8 +++ .github/workflows/workspace.yml | 118 ++++++++++++++++---------------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5d79390a2..fa2751233 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,10 @@ +# Font-build test fixtures are compared byte for byte: a checkout must not convert them. +crates/fonts-build/tests/fixture/** -text + +# The pinned font index is hashed and compiled in as committed: a checkout must not convert its +# line endings, or `Index::bundled` fails its checksum on Windows. +crates/font-store/index/sources.json -text +crates/font-store/index/indexes/*.gz binary + # Copies of the vendored krilla source diff, one per patch (see vendor/krilla/README.craft.md). vendor/krilla/patches/*.patch linguist-generated=true diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 4002900d9..1f66236e2 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -12,64 +12,66 @@ permissions: jobs: native: - name: Workspace (${{ matrix.os }}) - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Install software Vulkan renderer - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y mesa-vulkan-drivers libvulkan1 - echo 'WGPU_BACKEND=vulkan' >> "$GITHUB_ENV" - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 - with: - key: workspace - - name: Prepare the Windows test driver - if: runner.os == 'Windows' - shell: powershell - run: | - $ErrorActionPreference = 'Stop' - Start-Service Spooler - Add-PrinterDriver -Name 'Generic / Text Only' - - run: cargo fmt --check - - run: cargo clippy --workspace --all-targets --locked -- -D warnings - - run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - - run: cargo test --workspace --locked - env: - CRAFT_PRINT_REQUIRE_QUEUE: ${{ runner.os == 'Windows' && '1' || '0' }} - - run: cargo test --workspace --all-features --locked - env: - CRAFT_PRINT_REQUIRE_QUEUE: ${{ runner.os == 'Windows' && '1' || '0' }} + name: Workspace + uses: storytold/craft-common-actions/.github/workflows/gate.yml@main + with: + os: '["ubuntu-latest","windows-latest","macos-latest"]' + timeout-minutes: 60 + tools: "" + packages: mesa-vulkan-drivers libvulkan1 + craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83 + linux-env: WGPU_BACKEND=vulkan + windows-env: CRAFT_PRINT_REQUIRE_QUEUE=1 + env: CRAFT_PRINT_REQUIRE_QUEUE=0 + setup-command: | + set -euo pipefail + corpus_fetcher=.test-inputs/photocraft + mkdir -p "$corpus_fetcher" + git -C "$corpus_fetcher" init + git -C "$corpus_fetcher" remote add origin https://github.com/storytold/PhotoCraft.git + git -C "$corpus_fetcher" fetch --quiet --depth=1 origin 74330157cb601573ae4523a95bb9132822fc3918 + git -C "$corpus_fetcher" checkout --quiet --detach FETCH_HEAD + (cd "$corpus_fetcher" && cargo xtask corpus --psd --psd-tools --atn) + python scripts/test-fetch-pdf-test-fonts.py + python scripts/fetch-pdf-test-fonts.py "$RUNNER_TEMP/craft-pdf-test-fonts" + printf 'CRAFT_PSD_CORPUS=%s\n' "$GITHUB_WORKSPACE/$corpus_fetcher/corpus" >> "$GITHUB_ENV" + printf 'CRAFT_PDF_TEST_FONT_DIR=%s\n' "$RUNNER_TEMP/craft-pdf-test-fonts" >> "$GITHUB_ENV" + command: | + cargo fmt --check + cargo clippy --workspace --all-targets --locked -- -D warnings + cargo clippy --workspace --all-targets --all-features --locked -- -D warnings + cargo test --workspace --locked + cargo test --workspace --all-features --locked + windows-command: | + powershell.exe -NoProfile -Command '$ErrorActionPreference = "Stop"; Start-Service Spooler; Add-PrinterDriver -Name "Generic / Text Only"' + cargo fmt --check + cargo clippy --workspace --all-targets --locked -- -D warnings + cargo clippy --workspace --all-targets --all-features --locked -- -D warnings + cargo test --workspace --locked + cargo test --workspace --all-features --locked + msrv: - name: Rust 1.95 (Linux libraries) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.95.0 - - uses: Swatinem/rust-cache@v2 - with: - key: workspace-msrv - - run: cargo check --workspace --lib --all-features --locked + name: Rust 1.95 + uses: storytold/craft-common-actions/.github/workflows/gate.yml@main + with: + os: '["ubuntu-latest"]' + toolchain: 1.95.0 + targets: "" + tools: "" + craft-fonts-ref: "" + require-craft-fonts: false + timeout-minutes: 15 + dev-debug: "" + command: cargo check --workspace --lib --all-features --locked + wasm: name: UI and settings WASM - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 - with: - key: workspace-wasm - - run: cargo check -p craft-ui -p craft-settings --all-features --locked --target wasm32-unknown-unknown + uses: storytold/craft-common-actions/.github/workflows/gate.yml@main + with: + os: '["ubuntu-latest"]' + targets: wasm32-unknown-unknown + tools: "" + craft-fonts-ref: "" + require-craft-fonts: false + timeout-minutes: 15 + command: cargo check -p craft-ui -p craft-settings --all-features --locked --target wasm32-unknown-unknown From 7316e4e22314c2d8ce7d2cc8bc02b5e302b88e28 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 03:13:53 +0000 Subject: [PATCH 5/7] fix: clear workspace baseline gates on Rust 1.100 --- crates/cms/src/synth.rs | 8 ++-- crates/kra/src/lib.rs | 43 ++++++++++++++---- crates/latex/src/tests.rs | 12 ++--- crates/ora/src/lib.rs | 61 ++++++++++++++++++++------ crates/plotter/tests/golden.rs | 4 +- crates/proof/src/hunspell.rs | 16 ++++--- crates/proof/src/tr/mod.rs | 6 +-- crates/video/src/hevc/decode/filter.rs | 5 +-- 8 files changed, 108 insertions(+), 47 deletions(-) diff --git a/crates/cms/src/synth.rs b/crates/cms/src/synth.rs index a59058f08..b1535f78c 100644 --- a/crates/cms/src/synth.rs +++ b/crates/cms/src/synth.rs @@ -111,9 +111,9 @@ impl CmykModel { // Overprints with black: multiplicative in reflectance above a first-surface floor. let floor = 0.006; let mut prim = [[0.0; 3]; 16]; - for m in 0..16 { + for (m, slot) in prim.iter_mut().enumerate() { let p = base[m & 7]; - prim[m] = if m & 8 == 0 { + *slot = if m & 8 == 0 { p } else { std::array::from_fn(|i| { @@ -133,7 +133,7 @@ impl CmykModel { (v + 4.0 * self.tvi[i] * v * (1.0 - v)).clamp(0.0, 1.0) }); let mut s = [0.0; 3]; - for m in 0..16 { + for (m, prim) in self.prim_n.iter().enumerate() { let mut w = 1.0; for (i, ai) in a.iter().enumerate() { w *= if m & (1 << i) != 0 { *ai } else { 1.0 - ai }; @@ -141,7 +141,7 @@ impl CmykModel { if w == 0.0 { continue; } - for (acc, p) in s.iter_mut().zip(&self.prim_n[m]) { + for (acc, p) in s.iter_mut().zip(prim) { *acc += w * p; } } diff --git a/crates/kra/src/lib.rs b/crates/kra/src/lib.rs index 7485b9dd5..12ae3468b 100644 --- a/crates/kra/src/lib.rs +++ b/crates/kra/src/lib.rs @@ -161,6 +161,16 @@ fn children<'a>(el: &'a El, container: &'static str, tag: &'static str) -> impl } fn node(el: &El, is_mask: bool, count: &mut usize) -> Result { + let mut out = node_fields(el, is_mask)?; + if !is_mask { + out.layers = layers(el, count)?; + out.masks = children(el, "masks", "mask").map(|m| node(m, true, count)).collect::>()?; + } + Ok(out) +} + +/// A node's own attributes, without its children. +fn node_fields(el: &El, is_mask: bool) -> Result { let owned = |name: &str| attr(el, name).map(str::to_string); let node_type = attr(el, "nodetype").unwrap_or("").to_string(); // Only the kinds whose pixels are read need sound numbers; a node an application will leave @@ -168,7 +178,7 @@ fn node(el: &El, is_mask: bool, count: &mut usize) -> Result { let read = matches!(node_type.as_str(), "paintlayer" | "grouplayer" | "transparencymask"); let lenient = |v: Result, default: f64| if read { v } else { Ok(v.unwrap_or(default)) }; let (x, y) = if is_mask && !read { (offset(el, "x").unwrap_or(0), offset(el, "y").unwrap_or(0)) } else { (offset(el, "x")?, offset(el, "y")?) }; - let mut out = Node { + let out = Node { node_type, name: owned("name"), visible: attr(el, "visible") != Some("0"), @@ -187,22 +197,39 @@ fn node(el: &El, is_mask: bool, count: &mut usize) -> Result { layers: Vec::new(), masks: Vec::new(), }; - if !is_mask { - out.layers = layers(el, count)?; - out.masks = children(el, "masks", "mask").map(|m| node(m, true, count)).collect::>()?; - } Ok(out) } -/// The `` of `el`. Recursion is bounded by [`MAX_XML_DEPTH`]. +/// The `` of `el`, walked with an explicit stack: groups can nest up to [`MAX_XML_DEPTH`] +/// deep, more than a small thread stack can recurse. fn layers(el: &El, count: &mut usize) -> Result> { + struct Frame<'a> { + node: Node, + rest: std::vec::IntoIter<&'a El>, + } + let mut root = children(el, "layers", "layer").collect::>().into_iter(); + let mut stack: Vec = Vec::new(); let mut out = Vec::new(); - for child in children(el, "layers", "layer") { + loop { + let next = match stack.last_mut() { + Some(frame) => frame.rest.next(), + None => root.next(), + }; + let Some(child) = next else { + let Some(frame) = stack.pop() else { break }; + match stack.last_mut() { + Some(parent) => parent.node.layers.push(frame.node), + None => out.push(frame.node), + } + continue; + }; *count += 1; if *count > MAX_NODES { return Err(invalid(format!("more than {MAX_NODES} layers"))); } - out.push(node(child, false, count)?); + let mut built = node_fields(child, false)?; + built.masks = children(child, "masks", "mask").map(|m| node(m, true, count)).collect::>()?; + stack.push(Frame { node: built, rest: children(child, "layers", "layer").collect::>().into_iter() }); } Ok(out) } diff --git a/crates/latex/src/tests.rs b/crates/latex/src/tests.rs index 55bde2489..ccf349c80 100644 --- a/crates/latex/src/tests.rs +++ b/crates/latex/src/tests.rs @@ -353,9 +353,9 @@ fn table_borders_and_borderless_round_trip() { let mut doc = Document::new(); let mut t_borderless = Table::new(2, 2, 400.0); t_borderless.props.style = None; - for r in 0..2 { - for c in 0..2 { - t_borderless.rows[r].cells[c].blocks = vec![para_block(Paragraph::with_text("cell", CharProps::default()))]; + for row in &mut t_borderless.rows { + for cell in &mut row.cells { + cell.blocks = vec![para_block(Paragraph::with_text("cell", CharProps::default()))]; } } doc.body = vec![Arc::new(Block::Table(t_borderless))]; @@ -369,9 +369,9 @@ fn table_borders_and_borderless_round_trip() { let mut doc_bordered = Document::new(); let mut t_bordered = Table::new(2, 2, 400.0); t_bordered.props.style = Some("TableGrid".to_string()); - for r in 0..2 { - for c in 0..2 { - t_bordered.rows[r].cells[c].blocks = vec![para_block(Paragraph::with_text("cell", CharProps::default()))]; + for row in &mut t_bordered.rows { + for cell in &mut row.cells { + cell.blocks = vec![para_block(Paragraph::with_text("cell", CharProps::default()))]; } } doc_bordered.body = vec![Arc::new(Block::Table(t_bordered))]; diff --git a/crates/ora/src/lib.rs b/crates/ora/src/lib.rs index b2a91cc40..69d494d0d 100644 --- a/crates/ora/src/lib.rs +++ b/crates/ora/src/lib.rs @@ -189,28 +189,63 @@ fn props(el: &El) -> Result { }) } -/// The elements of `stack`. Recursion is bounded by [`MAX_XML_DEPTH`]. +/// The elements of `stack`, walked with an explicit stack: a stack can nest up to +/// [`MAX_XML_DEPTH`] deep, more than a small thread stack can recurse. fn stack(el: &El, count: &mut usize) -> Result> { + struct Frame<'a> { + props: Props, + isolation: Option, + children: Vec, + rest: std::vec::IntoIter<&'a El>, + } + let mut root = el.elements().collect::>().into_iter(); + let mut frames: Vec = Vec::new(); let mut out = Vec::new(); - for child in el.elements() { + loop { + let next = match frames.last_mut() { + Some(frame) => frame.rest.next(), + None => root.next(), + }; + let Some(child) = next else { + let Some(frame) = frames.pop() else { break }; + let node = Node::Stack(Stack { props: frame.props, isolation: frame.isolation, children: frame.children }); + match frames.last_mut() { + Some(parent) => parent.children.push(node), + None => out.push(node), + } + continue; + }; *count += 1; if *count > MAX_NODES { return Err(invalid(format!("more than {MAX_NODES} layers"))); } - out.push(match child.name.as_str() { - "layer" => Node::Layer(Layer { - props: props(child)?, - x: int(child, "x")?, - y: int(child, "y")?, - src: child.attr("src").map(|s| s.trim().to_string()), - }), - "stack" => Node::Stack(Stack { + match child.name.as_str() { + "layer" => { + let node = Node::Layer(Layer { + props: props(child)?, + x: int(child, "x")?, + y: int(child, "y")?, + src: child.attr("src").map(|s| s.trim().to_string()), + }); + match frames.last_mut() { + Some(parent) => parent.children.push(node), + None => out.push(node), + } + } + "stack" => frames.push(Frame { props: props(child)?, isolation: child.attr("isolation").map(|s| s.trim().to_string()), - children: stack(child, count)?, + children: Vec::new(), + rest: child.elements().collect::>().into_iter(), }), - other => Node::Other { tag: other.to_string(), name: child.attr("name").map(str::to_string) }, - }); + other => { + let node = Node::Other { tag: other.to_string(), name: child.attr("name").map(str::to_string) }; + match frames.last_mut() { + Some(parent) => parent.children.push(node), + None => out.push(node), + } + } + } } Ok(out) } diff --git a/crates/plotter/tests/golden.rs b/crates/plotter/tests/golden.rs index 26aaae554..4d5029010 100644 --- a/crates/plotter/tests/golden.rs +++ b/crates/plotter/tests/golden.rs @@ -135,7 +135,7 @@ fn silhouette_registration_marks() { let p = silhouette::encode(&pro, &j).unwrap(); let (head, rest) = p.split_at_registration(); let text = |c: &[Command]| c.iter().map(Command::to_ascii).collect::>().join(" "); - assert!(text(head).ends_with(" FC18,1,1 TB50,0 TB99 TB52,2 TB51,400 TB53,10 TB55,1 TB123,4600,3600,200,100")); + assert!(text(head).ends_with(" FC18,1,1 TB50,0 TB99 TB52,2 TB51,400 TB53,10 TB54,0,0 TB55,1 TB123,4600,3600,200,100")); // The design is shifted so the first mark is the origin. assert_eq!(text(rest), "M100,100 D100,300 M100,0 SO0"); assert_eq!(p.registration_wait, Some(head.len())); @@ -148,7 +148,7 @@ fn silhouette_registration_marks() { reg.origin_right_mm = 0.0; assert!(ascii(®).ends_with(" TB124,4600,3600,0,0")); reg.search = false; - assert_eq!(ascii(®), "TB50,0 TB99 TB52,2 TB51,400 TB53,10 TB55,1 TB23,4600,3600"); + assert_eq!(ascii(®), "TB50,0 TB99 TB52,2 TB51,400 TB53,10 TB54,0,0 TB55,1 TB23,4600,3600"); } #[test] diff --git a/crates/proof/src/hunspell.rs b/crates/proof/src/hunspell.rs index 966ded72f..08629a052 100644 --- a/crates/proof/src/hunspell.rs +++ b/crates/proof/src/hunspell.rs @@ -754,11 +754,12 @@ mod tests { /// spec's default `~/.local/share` applies, and with no home there is no folder. #[test] fn xdg_data_home_follows_the_spec() { - let home = Path::new("/home/a"); - assert_eq!(xdg_data_home(Some("/data".into()), Some(home)), Some(PathBuf::from("/data"))); - assert_eq!(xdg_data_home(Some("/data".into()), None), Some(PathBuf::from("/data"))); + let home = std::env::temp_dir().join("xdg-home"); + let data = std::env::temp_dir().join("xdg-data"); + assert_eq!(xdg_data_home(Some(data.as_os_str().to_owned()), Some(&home)), Some(data.clone())); + assert_eq!(xdg_data_home(Some(data.as_os_str().to_owned()), None), Some(data)); for xdg in [None, Some(""), Some("relative/data")] { - assert_eq!(xdg_data_home(xdg.map(Into::into), Some(home)), Some(home.join(".local/share")), "{xdg:?}"); + assert_eq!(xdg_data_home(xdg.map(Into::into), Some(&home)), Some(home.join(".local/share")), "{xdg:?}"); assert_eq!(xdg_data_home(xdg.map(Into::into), None), None, "{xdg:?}"); } } @@ -1010,7 +1011,12 @@ mod tests { assert!(load(&aff, &big).unwrap_err().contains("too large")); // A directory, and on Unix an endless device, are not dictionary files. (No clock: a // reader that read `/dev/zero` would never return, so the error itself is the proof.) - assert!(load(&aff, &dir).unwrap_err().contains("not a regular file")); + let error = load(&aff, &dir).unwrap_err(); + // Windows may reject opening a directory before the reader can inspect its type. + #[cfg(unix)] + assert!(error.contains("not a regular file"), "{error}"); + #[cfg(not(unix))] + assert!(!error.is_empty()); #[cfg(unix)] assert!(load(&aff, Path::new("/dev/zero")).unwrap_err().contains("not a regular file")); // Turkish reads through the same reader. diff --git a/crates/proof/src/tr/mod.rs b/crates/proof/src/tr/mod.rs index 5472fbe21..c728d5872 100644 --- a/crates/proof/src/tr/mod.rs +++ b/crates/proof/src/tr/mod.rs @@ -100,11 +100,7 @@ fn check(d: &TrDict, word: &str, opts: &crate::Options) -> bool { } // Compound words separated by hyphens (e.g., `afyon-karahisar`) - if w.contains('-') && w.split('-').all(|part| part.is_empty() || check(d, part, opts)) { - return true; - } - - false + w.contains('-') && w.split('-').all(|part| part.is_empty() || check(d, part, opts)) } #[cfg(test)] diff --git a/crates/video/src/hevc/decode/filter.rs b/crates/video/src/hevc/decode/filter.rs index d5b94161d..69f936bc1 100644 --- a/crates/video/src/hevc/decode/filter.rs +++ b/crates/video/src/hevc/decode/filter.rs @@ -522,10 +522,7 @@ fn sao_edge_ctb( return false; } } - if !pic.pps.loop_filter_across_tiles && pic.layout.tile_id[pic.layout.rs_to_ts[nrs] as usize] != pic.layout.tile_id[cur_ts as usize] { - return false; - } - true + pic.pps.loop_filter_across_tiles || pic.layout.tile_id[pic.layout.rs_to_ts[nrs] as usize] == pic.layout.tile_id[cur_ts as usize] }; // avail[dy][dx]: the CTB at (rx + dx - 1, ry + dy - 1) may be used. let (rx, ry) = (rs % pic.wctb, rs / pic.wctb); From 4c251094737d9931a79cc8a68fdf5eb39d25b585 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 03:47:36 +0000 Subject: [PATCH 6/7] test(openpgp): align signature time with generated key --- crates/openpgp/src/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/openpgp/src/tests.rs b/crates/openpgp/src/tests.rs index 4dc30a0d3..f78519ba0 100644 --- a/crates/openpgp/src/tests.rs +++ b/crates/openpgp/src/tests.rs @@ -443,7 +443,8 @@ fn the_unhashed_area_never_decides_who_is_blamed() { // An issuer only in the unhashed area finds a key, which counts because it fits. let alice = secret(ALICE, Make::default()); - let mut config = config(&alice.primary_key, pgp::packet::SignatureType::Binary, t, vec![]); + let alice_created = made(&alice); + let mut config = config(&alice.primary_key, pgp::packet::SignatureType::Binary, alice_created, vec![]); config.hashed_subpackets.retain(|p| matches!(p.data, SubpacketData::SignatureCreationTime(_))); config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::IssuerFingerprint(alice.primary_key.fingerprint())).unwrap()]; let sig = config.sign(&alice.primary_key, &Password::empty(), SIGNED_PART.as_bytes()).unwrap(); From ca7a73bd2c812c6c37f0b86827eba6d0749e4912 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 04:50:46 +0000 Subject: [PATCH 7/7] ci: allow longer Windows workspace gate --- .github/workflows/workspace.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 1f66236e2..cf9499419 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -16,7 +16,7 @@ jobs: uses: storytold/craft-common-actions/.github/workflows/gate.yml@main with: os: '["ubuntu-latest","windows-latest","macos-latest"]' - timeout-minutes: 60 + timeout-minutes: 120 tools: "" packages: mesa-vulkan-drivers libvulkan1 craft-fonts-ref: b076417b5880bd4154bd22f00695b1d28c6bea83