Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions ATTRIBUTION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a3234684d5318ce38213c79533d3eebb9c635497ba9e00fb9ca7ebe2fad9d73d"
sha256 = "f9f205314ad16f0dabf28241e2d791db0d498c9cea342964312671975c6511f7"

# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
Expand All @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c5e0206846b009bcb366d092c3d64fd8b9e42d29d5406fb73ae36ecb1f2083d5"
sha256 = "90c9ca19ceb74b843823b39d1dc8331b2c8a1ca06ab93699a6fed1711d4fd113"

[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
Expand All @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "349a2ead371bdbfeefb9ab2279c676b205cefe163cbef174f8123fe4bf739ab9"
sha256 = "dff858c8d5c52cd16b955e867e59b8965d0bec4fc72bb31894200d33a0b7996b"

[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
Expand All @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "cb332907792d4aa760ca9862160e24a7b469afdbcfb93235e99deb8d58604ae2"
sha256 = "86bf33f1f9e04748945d7980e9562c959aef27d6280890b8ab3193b277c6e0cf"

[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
Expand All @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ebd3a12e471ba9d9a69e6532fbac2d027dec3c91b79cdfbb1c6c386d13de3bd6"
sha256 = "687078cf5a1feb641c8562c2ac6ebe1b3edc061d9ef4add77808739cca4c76c7"

[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
Expand All @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f5e0553165ab0223bfbc809b1630170d1af11d6546ae5a682d4090104b6e4a05"
sha256 = "635c096668b3609d6e4b00e4309f661de9c72b3a60a846f5bf5ee5e3c9978c1e"

[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
Expand All @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "dd2171a0a940a84e512fd6408a11cddf104be897ec4ab670c5ef6f1781ccb671"
sha256 = "feff3218c4c922d7ebc5fee64630439a33c44b336547233cec06452a59f5f28c"

[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
Expand All @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1b9b3d0d6dab626c3acb8c9aa64d64e56b98eb3f2faf71dcb1957067e72706db"
sha256 = "0b275e7c28178d3fccdfdbf4fe36ee98776b217b1cdc05a73b3dc190005f18b3"

[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
Expand All @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "668f23f4095120343e3f0910dc8f5844765370fae40400c48d685cad20dcf60e"
sha256 = "993a9defca9ec79841494c31c7b02ccc3e17882572a43d7c71b87e77785262a9"

[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
Expand All @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "02f3fd4b2721b0ea9c01c99d477a0339a28180514568179d3fc420e7fb81c671"
sha256 = "b6d048d14fcc7f640378ba1e2df5772149bec4f85dd4dca14f2fdf94a34c5295"

[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
Expand All @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "26eb438c309d34e55eba959a21f139db91fa5fd1d6d4b7c3d1cd0fa79a60a5b0"
sha256 = "52f92ca859f5a3d1c0d131693a7c9eb6082f137b69241acc864a060866e7fca6"

[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
Expand All @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c04b6d0320bce999b33bd189bffcec8a196d4169e56cc65675dc920c5a671dac"
sha256 = "8abf3af6fba89b62ecaffdc7dbe01278af33003fc6dc76e7cc30736309870d3a"

[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
Expand All @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "37b042977d934b0b550e5f52faa5d90abbcb9e7ff61ff9588021610ce709e474"
sha256 = "740ff17e73d141162c9151a8218ef0a7e62ec832c1acf60db394e0ad8bb3e261"

[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
Expand All @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "3d1f1d2558eec1271fa653bc38616feeefc6559deeb2a0cdbf07d67634e9ac97"
sha256 = "702f02e5edf341e17243984f4e8ebfa3573ba520c3fdd4031818d57b477e64eb"

[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
Expand All @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "03071249bd6c13f79da72262415e484fb1aa36098a37a49094b9dab58cebc4e2"
sha256 = "edd5f901bbb5dc4741bfcdb4af814a2594a2928cdb4f983cae972822d4d03641"

[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
Expand All @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c0ba73b27b95b275f5eebc040ab1c3c6c3b227c63e1c637b4cdb27d0a9264a8b"
sha256 = "460661fea9a853f219039bd1210120887da406d31eef82b59f431a34396f0229"

[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
Expand All @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e3af76714ee9899328a453ec83195f4dc462fe48ba445fbe3688c777e74bc9dc"
sha256 = "68d596c15da2af43473fc0a4128a857525fb3a6a1e8523ca3944c2753889ce7f"

[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
Expand All @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0855d7f785c433799da83400ebc932f987eb543008486bc4f553aadb4102c2ba"
sha256 = "a8067bec58077aeeea471dd08718efb23c88e73fdcce2121002188c97729cdcf"

[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
Expand All @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "884ab18d60db52f65cd0e30530e196c73c0e627f450bc948517e2f4b7deeb04b"
sha256 = "9a0ff321d5091352550c5c21352a402cf6c96b7e3c1ed7a71c481ec0b63f6d2c"

[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
Expand All @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c805a2c6da08e77a9a7455856d82c168cf6f353d96e1dc140fcc8898f12ba538"
sha256 = "eb25e9361e40a45f5259c16ff44f1915621cbc899786298eb4e967e03e97f863"


[[asset]]
Expand Down Expand Up @@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "11619064ae9edaeee5d601b7008e40095a40375a3e1a298155d59cb758fe7d34"
sha256 = "addc737262eacbf2122425629a7ec41f269d0e55e1d7b94e37b112810552a674"

[[asset]]
path = "crates/ui-egui/src/i18n/sl.tsv"
Expand All @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "89f2c7495cfb0fc9d36d35b95c056dbeda18ccae521250babac719ff5df62d89"
sha256 = "66fa8fb4beaa83e07087e9a3af9883816ffea379e4bd08af7ee22ed22654cac8"

[[asset]]

Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-10-15 (M6, AcroForm XML text):** Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. Synthetic regressions verify exact values and unknown catalog data after incremental and full save/reopen. Nested-element and XFA interpretation are unchanged. Estimates unchanged.
- **2026-10-15 (M6, #1917 refresh):** Integrated upstream `99941daa` while retaining the XML/XFDF attribute whitespace correction and all upstream progress. Only independent documentation conflicts required resolution; focused production and test files are unchanged from the previously green CI head. Estimates unchanged.
- **2026-10-15 (M1, #2267 refresh):** Integrated upstream `99941daa` while retaining the raw-deflate fallback correction and all upstream progress. Only independent documentation conflicts required resolution; focused production and test files are unchanged from the previously green CI head. Estimates unchanged.
- **2026-10-16 (community, M10, #2684):** Printing to a Windows printer sends the pages that draw with Type 3 fonts (as Chrome writes them) as pictures at the Print as image resolution, and keeps the other pages vector: the Windows print component could close the whole app on such text, natively, where nothing can catch it. Type 3 use is read from each page's resources, forms, tiling patterns, soft masks and printing annotations, with a depth limit and a budget; the Print dialog says so, the log names the pages, and `doc_print type3_as_image` does it headlessly. The crash itself was not reproduced here. Estimates unchanged.
- **2026-10-15 (M2, gap 12):** `cargo xtask text-oracle` scores reading order as well as word F1: the longest common subsequence of the two word sequences over the longer one. F1 is a bag of words and cannot see order at all -- measuring #2431, a change that moves text and nothing else, it called 26 of the 29 files whose output changed identical, where the order score separated 18 improvements from 6 regressions. Reports, does not gate; a disagreement with Poppler, not a verdict. Estimates unchanged.
- **2026-10-15 (M2, #2305 follow-up):** WinAnsiEncoding 0xA0 and 0xAD and MacRomanEncoding 0xCA extract as the ordinary space and hyphen Annex D.2 names them, in the vendored hayro tables (patch 30) as well as `crates/fonts`, so search, copy and redaction see characters that were hidden behind U+00A0 and U+00AD. Across the pdf.js corpus the soft hyphens we extract fall 117 to 13 and the non-breaking spaces 30 to 26; the rest come from `/ToUnicode`. An explicit `/Differences` name keeps its glyph list value, as pdftotext does. Estimates unchanged.
- **2026-10-13 (community, M3, #1569 review):** Pixel-only Wayland wheel, wheel-tilt and continuous input use standalone moved events, so Ctrl zoom does not remain latched after the modifier is released (#991); vendored winit patch (13), with a policy test. Estimates unchanged.
Expand Down
1 change: 1 addition & 0 deletions crates/automation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mcp = ["dep:base64"]

[dev-dependencies]
pdfcraft-fonts = { workspace = true }
pdfcraft-testkit = { workspace = true }
flate2.workspace = true
pdfcraft-model = { workspace = true }
pdfcraft-xfa = { workspace = true }
Expand Down
21 changes: 20 additions & 1 deletion crates/automation/src/printing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,30 @@ impl Automation {
} else {
None
};
let settings = print::Settings { pages, paper, orientation, layout, content, raster, crops: Vec::new() };
// The pages that draw with Type 3 fonts as pictures and the others as they are: by default
// when printing to a Windows printer, whose print component can crash on them (#2684).
let type3_raster = match a.opt_bool("type3_as_image")?.unwrap_or(spool::TYPE3_AS_IMAGE && a.opt_str("printer")?.is_some()) {
true if raster.is_none() => {
let dpi = a.opt_num("dpi")?.unwrap_or(300.0);
if !(72.0..=600.0).contains(&dpi) {
return Err(bad("dpi must be between 72 and 600"));
}
Some(print::Raster { dpi, grayscale: a.opt_bool("grayscale")?.unwrap_or(false) })
}
_ => None,
};
let settings = print::Settings { pages, paper, orientation, layout, content, raster, type3_raster, crops: Vec::new() };
let sizes: Vec<(f64, f64)> = self.doc(a)?.info.pages.iter().map(|p| (p.width as f64, p.height as f64)).collect();
let sheets = print::layout(&sizes, &settings).map_err(|e| bad(e.to_string()))?.len();
let as_images = match settings.type3_raster {
Some(_) => self.session.print_type3_pages(id, &settings.pages, settings.content),
None => Vec::new(),
};
let bytes = self.session.print_pdf(id, &settings).map_err(failed)?;
let mut out = json!({ "sheets": sheets, "pages": settings.pages.len(), "bytes": bytes.len() });
if !as_images.is_empty() {
out["type3_as_image"] = json!(as_images.iter().map(|p| p.saturating_add(1)).collect::<Vec<_>>());
}
match (a.opt_str("path")?, a.opt_str("printer")?) {
(Some(p), None) => {
let path = self.resolve(p, true)?;
Expand Down
3 changes: 2 additions & 1 deletion crates/automation/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ pub fn tools() -> Vec<ToolDef> {
"duplex": { "type": "string", "enum": ["off", "long-edge", "short-edge"] },
"grayscale": { "type": "boolean" },
"as_image": { "type": "boolean", "description": "Print as image (Acrobat's Advanced Print Setup): every page rendered to a picture at dpi before it is placed on the paper, for printers that skip text, shading or transparency; the print is no longer searchable text, and with grayscale the pictures are grey." },
"dpi": { "type": "number", "minimum": 72, "maximum": 600, "description": "Resolution for as_image (default 300)." },
"dpi": { "type": "number", "minimum": 72, "maximum": 600, "description": "Resolution for as_image and type3_as_image (default 300)." },
"type3_as_image": { "type": "boolean", "description": "Print the pages that draw with Type 3 fonts as pictures at dpi and the other pages as they are; the result lists those pages (1-based) as type3_as_image. On by default when printing to a printer on Windows, whose print component can crash on Type 3 text; off otherwise." },
"options": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Printer driver options from printer_options: key → choice value." },
}),
&["doc"],
Expand Down
44 changes: 44 additions & 0 deletions crates/automation/tests/automation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6546,6 +6546,50 @@ fn doc_print_as_image() {
}
}

/// `doc_print` with `type3_as_image` sends the pages that draw with Type 3 fonts as pictures and
/// names them, while the other pages stay text (#2684: on by default for Windows printers only, so
/// a saved print-ready PDF keeps every page as it is unless asked).
#[test]
fn doc_print_sends_type3_pages_as_images() {
let dir = workdir("print-type3");
let pdf = pdfcraft_testkit::Pdf::new()
.obj("<< /Type /Catalog /Pages 2 0 R >>")
.obj("<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 200 300] >>")
.obj("<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << /XObject << /Fm 7 0 R >> >> >>")
.obj("<< /Type /Page /Parent 2 0 R /Contents 6 0 R /Resources << /Font << /F1 11 0 R >> >> >>")
.stream("", "q /Fm Do Q")
.stream("", "BT /F1 24 Tf 20 150 Td (Page 2) Tj ET")
.stream("/Type /XObject /Subtype /Form /BBox [0 0 200 300] /Resources << /Font << /F9 8 0 R >> >>", "BT /F9 40 Tf 30 200 Td <010F01> Tj ET")
.obj(
"<< /Type /Font /Subtype /Type3 /FontMatrix [0.001 0 0 0.001 0 0] /FontBBox [0 0 600 700] /FirstChar 0 /LastChar 15 \
/Widths [0 600 0 0 0 0 0 0 0 0 0 0 0 0 0 250] /Encoding << /Differences [1 /g3B 15 /g3] >> /CharProcs << /g3B 9 0 R /g3 10 0 R >> >>",
)
.stream("", "600 0 0 0 600 700 d1\n0 0 600 700 re f")
.stream("", "250 0 0 0 0 0 d1")
.obj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
.build();
std::fs::write(dir.join("chrome.pdf"), pdf).unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "chrome.pdf" }))["doc"].as_u64().unwrap();
let pictures = |name: &str| {
let out = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(dir.join(name)).unwrap())).unwrap();
out.scan_objects().filter(|(_, o)| matches!(&**o, pdfcraft_cos::Object::Stream(st) if st.dict.name(b"Subtype") == Some(b"Image"))).count()
};
let r = ok(&mut a, "doc_print", json!({ "doc": doc, "type3_as_image": true, "dpi": 100, "path": "mixed.pdf" }));
assert_eq!(r["type3_as_image"], json!([1]), "{r}");
assert_eq!(pictures("mixed.pdf"), 1);
let mixed = ok(&mut a, "doc_open", json!({ "path": "mixed.pdf" }))["doc"].as_u64().unwrap();
assert_eq!(page_text(&mut a, mixed), ["", "Page 2"]);
let r = ok(&mut a, "doc_print", json!({ "doc": doc, "path": "plain.pdf" }));
assert!(r.get("type3_as_image").is_none(), "{r}");
assert_eq!(pictures("plain.pdf"), 0);
match a.call("doc_print", &json!({ "doc": doc, "type3_as_image": true, "dpi": 20, "path": "x.pdf" })) {
Err(ToolError::InvalidArgs(m)) => assert!(m.contains("dpi"), "{m}"),
other => panic!("{other:?}"),
}
let _ = std::fs::remove_dir_all(dir);
}

/// Without a root, `write_output` still writes through a staging file (#2166): the folder is
/// made, nothing is left beside the file, and a folder at the path is refused.
#[test]
Expand Down
3 changes: 3 additions & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ idna = "1.1"
icu_properties = "2.3"
unicode-segmentation = "1.13"

[dev-dependencies]
pdfcraft-testkit = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Already in the graph through lopdf; used only for the local UTC offset ("today").
chrono = { version = "0.4", default-features = false, features = ["clock"] }
Loading
Loading