diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 0e01f271d3..d097552982 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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 = "348b7dbb18ca419dbdaf05c6701bb96754a5989c178bec1e971164e2e15c6c43" +sha256 = "0c9691685fecfc582ae81c748bcbf645044722e3c233d4502cb49b5a98f93af9" # 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`). @@ -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 = "bea733dbdd4cdcdd2f9c1d7c278a382ee51188ee37f01c8a049c35e953151869" +sha256 = "219efe9f342aa269d4ecfef9617fa39064ea63ac7148231493068470f8257700" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -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 = "6d5f5923f73d01fa6166a43b7221834ebb39002ce2d826084c10a4d3d89526a9" +sha256 = "e730271977b4d89e30f2d822e65f8894b832a1b2ba66f816828a106cc6b20e8e" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -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 = "9cea4cc5aa0289e609b00779acc1709c33488fe0a0b59d60a124e877c714eedc" +sha256 = "a209f68c88cab1c19e095e9e12bc6f9251bbabc0a411d0e5aba7c1399c94159e" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -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 = "b6c62e0b859917175c80d7fa0a9174a3d445458b8d16ff85260a224ccc10204d" +sha256 = "d174236d0bf87e2c212fa4c4c1b7cd2a2438150a13af0a2ca08194170d192b8d" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -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 = "8ee80cd976b0a43f601628fa48a9b09bcc4a1e66b4df6870597ee0b1027d897e" +sha256 = "b588396c7c4972709843ea3bfc42213780a11c92b83d52292926b6eb9ed52167" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -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 = "656b2e9550c747e44bffd2437443a5dcd946841831e9708bcb93c5dd919221f5" +sha256 = "762a8220093ad1a51126f850e6c8692067a626f528e2cd88b40a1f04d9482c9a" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -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 = "f7bb9f7b48c8759ca2e9a4fac8d991b5d656ef0c0563b81b320b4d127bb474a9" +sha256 = "61c4bdac7f60d924ec548c84d04b4c9ef137f27096f958f3584207b50c8e91d5" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -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 = "e2ab1fe22706c4121c7e9a0d56da6404e61d7430433b915f22129c4733da3ed1" +sha256 = "91808c674033322c15fa4b40f9175551bcd66bdc02769a90df04c3fa33d710ef" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -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 = "657dcdbecc7e105e03a82a0f7903b0e51c4120f8c4a793254216df989fb1f893" +sha256 = "70ae72cd45e6084be65cda4adbfbc1fba31dc92ef46ece38064f4a21755d096b" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -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 = "75282238445a2873e322059a90795463033ec4dfbc30b83c21d20bcde3680f8f" +sha256 = "94b507ad2d09e04339d4307ea44281c0bb27eb5f04f616c4e9e9bb69613fd5cb" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -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 = "653525be13d7fb18b3dcc6eb05656bb9d2c573dea2c488b21aa82b319ed4b67c" +sha256 = "b69396f3c0049eaf788fcd742d067857dacb0ae61fb84e22888373b647a62c1c" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -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 = "d49788ad569aee627cd4386799b58334f03115c2e6977af3f02305a6771bc403" +sha256 = "778e2c93af6178fa9ec3544d10716174259a8587cf0912b0d5ab4c769b50a514" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -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 = "5fa60b06689a8c33fb0540dd3cd4c996064629066b2b116a3ded535c73405192" +sha256 = "1d8ef2cb8b2010178bf5036c4563dec00bfca8d5ef1ad436dacf6412567e74d7" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -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 = "2ac70113c4dd3182c566681f301f0fa0da77c57231ddcad514db6f67cd4bb8e9" +sha256 = "d722c9d6d0427b9f50119df0cde7b4f3341779bdfaa349460b68f02bc21ca5f8" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -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 = "c61ad71750d8246f09d8e9ee1229d77662b96c7dba886e3ce7b46083e3bac406" +sha256 = "79033f5db7d2a5ab1aebaa6cadb77806d45d072fdf250bd2086974a77f4bf7ce" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -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 = "f675541c61cf5fb70cc63e95ffe1e56c5c63e9c311d1c321852aa2e3313b7833" +sha256 = "fe159f37f57f1d49e6a80b6b971f799a19a49b6feaf91d40424d0d5837e8bfb5" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -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 = "cdb9a5d7e51db6b62c1d85a77e42faa73932425791b06fad836333f1ee21b7fe" +sha256 = "79332492b077695340b023c6e0b33523ae40f7a77f88eedc739cb8cff4c6b29e" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -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 = "d4d0591fab5d328a5d34346ffc7abe1dbaeed8241a97f9624763b95126ab78fa" +sha256 = "5eaa8ca29239cd92d64e343e5a2809a5e02239ccd06e7ac16774335328f01c80" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -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 = "d2d0588abbb6ae03347d197c58dc18d97a18bc479580216a14501be292254e8f" +sha256 = "122781df7711e41ce2462eea032133c449c5cafbdb528dc7fd096d7ff033e587" [[asset]] @@ -3363,4 +3363,4 @@ 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 = "2a2d8aa07d2c6de2f55b9d7de1fb299c63843c217013c930c288be4ce68c6b87" +sha256 = "c81dcea055935312c77aea40928748a95f179d85a0418968d2893a479a1272bd" diff --git a/crates/automation/src/printing.rs b/crates/automation/src/printing.rs index e9b16fc8ad..9ecf7d3d1f 100644 --- a/crates/automation/src/printing.rs +++ b/crates/automation/src/printing.rs @@ -125,7 +125,18 @@ impl Automation { bad(format!("unknown paper {p:?}; choose one of {}", PAPERS.iter().map(|(name, _)| *name).collect::>().join(", "))) })?, }; - let settings = print::Settings { pages, paper, orientation, layout, content }; + // Print as image (Acrobat's Advanced Print Setup): pictures at `dpi`, grey when + // `grayscale` is also asked for. + let raster = if a.opt_bool("as_image")?.unwrap_or(false) { + 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) }) + } else { + None + }; + let settings = print::Settings { pages, paper, orientation, layout, content, raster }; 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 bytes = self.session.print_pdf(id, &settings).map_err(failed)?; diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index d49fd7d433..0475d84656 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -512,6 +512,8 @@ pub fn tools() -> Vec { "collate": { "type": "boolean" }, "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)." }, "options": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Printer driver options from printer_options: key → choice value." }, }), &["doc"], diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 65b8265e99..ba6780a281 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -6094,3 +6094,23 @@ fn a_missing_path_under_the_root_names_where_it_was_looked_for() { ok(&mut a, "doc_open", json!({ "path": "inside.pdf" })); let _ = std::fs::remove_dir_all(base); } + +/// `doc_print` prints as image: pictures at `dpi`, grey with `grayscale`; a resolution outside +/// 72–600 is refused. +#[test] +fn doc_print_as_image() { + let dir = workdir("print-image"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + let r = ok(&mut a, "doc_print", json!({ "doc": doc, "pages": "1", "as_image": true, "dpi": 150, "grayscale": true, "path": "image.pdf" })); + assert_eq!(r["sheets"], 1); + let bytes = std::fs::read(dir.join("image.pdf")).unwrap(); + let out = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).unwrap(); + let pictures = + out.scan_objects().filter(|(_, o)| matches!(&**o, pdfcraft_cos::Object::Stream(st) if st.dict.name(b"Subtype") == Some(b"Image"))).count(); + assert_eq!(pictures, 1); + match a.call("doc_print", &json!({ "doc": doc, "as_image": true, "dpi": 20, "path": "x.pdf" })) { + Err(ToolError::InvalidArgs(m)) => assert!(m.contains("dpi"), "{m}"), + other => panic!("{other:?}"), + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index af5718b572..831d24e584 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -28,6 +28,7 @@ pub mod js; pub mod links; pub mod ocr; pub mod optimizer; +pub mod print_raster; pub mod printcheck; mod scan_colours; mod scan_fields; @@ -4104,6 +4105,26 @@ impl Session { if !doc.allows_printing() { return Err(EditError::NotPermitted("printing")); } + // Print as image: the pages used become pictures first, then the picture document is + // imposed in their place (its page k is the k-th distinct source page used). + if let Some(raster) = &settings.raster { + let mut distinct: Vec = Vec::new(); + for &p in &settings.pages { + if !distinct.contains(&p) { + distinct.push(p); + } + } + let hide_comments = matches!(settings.content, print::Content::Document | print::Content::FormFieldsOnly); + let bytes = print_raster::rasterize(doc, &distinct, raster, hide_comments).map_err(EditError::Print)?; + let pictures = pdfcraft_cos::Document::open(Arc::new(bytes)).map_err(|e| EditError::Print(e.to_string()))?; + let remapped = print::Settings { + pages: settings.pages.iter().map(|p| distinct.iter().position(|d| d == p).unwrap_or(0)).collect(), + content: print::Content::Document, + raster: None, + ..settings.clone() + }; + return pdfcraft_print::impose(&pictures, &remapped).map_err(|e| EditError::Print(e.to_string())); + } let cos = match doc.editor.as_ref() { Some(e) => e.cos.clone(), None => { diff --git a/crates/engine/src/print_raster.rs b/crates/engine/src/print_raster.rs new file mode 100644 index 0000000000..117c8e14cc --- /dev/null +++ b/crates/engine/src/print_raster.rs @@ -0,0 +1,61 @@ +//! Print as image (Acrobat's Advanced Print Setup ▸ Print As Image): every page +//! sent to the printer as a picture at a chosen resolution, for printers that skip text, +//! shading or transparency. The pages are rendered the way the viewer shows them (reading +//! colours off, the print's own colours), made into an image-only document, and that document +//! is imposed on the paper like any other. The print is no longer searchable text. + +use pdfcraft_create::ImageResolution; +use pdfcraft_print::Raster; +use pdfcraft_render::{PRINT_TAG, PageRenderer, RenderRequest}; + +use crate::Document; +use crate::export::encode_png; + +/// `doc`'s pages `pages` (0-based, each once, in the order given) as an image-only PDF, one +/// page per entry, the size of the source page as displayed. Pages too large for the renderer +/// at `raster.dpi` render at the largest resolution it allows. +pub fn rasterize(doc: &Document, pages: &[usize], raster: &Raster, hide_comments: bool) -> Result, String> { + let dpi = raster.dpi.clamp(72.0, 600.0); + let src = doc.export_source(); + let mut config = src.config.clone(); + config.hide_comments = hide_comments; + let mut renderer = PageRenderer::new(src.bytes, config); + let mut images: Vec<(String, Vec)> = Vec::with_capacity(pages.len()); + let mut used: Option = None; + for &page in pages { + let (w, h) = src.sizes.get(page).copied().ok_or_else(|| format!("page {} does not exist", page + 1))?; + let scale = pdfcraft_render::effective_scale(w, h, (dpi / 72.0) as f32); + let page_dpi = f64::from(scale) * 72.0; + // Every page of the image document shares one resolution, so its pages keep the + // source's sizes; the lowest a page needed is used for all. + used = Some(used.map_or(page_dpi, |u: f64| u.min(page_dpi))); + let r = renderer.render(RenderRequest { page, scale, tag: PRINT_TAG, ..Default::default() }); + if let Some(e) = r.error { + return Err(format!("page {}: {e}", page + 1)); + } + let mut rgba = r.rgba.to_vec(); + over_white(&mut rgba, raster.grayscale); + images.push((format!("page-{}.png", page + 1), encode_png(r.width, r.height, &rgba)?)); + } + let dpi = used.unwrap_or(dpi); + let doc = pdfcraft_create::from_images_with_resolution(&images, ImageResolution::Dpi(dpi)).map_err(|e| e.to_string())?; + pdfcraft_cos::write_full(&doc, &pdfcraft_cos::SaveOptions::default()).map_err(|e| e.to_string()) +} + +/// Premultiplied RGBA composed over white paper (opaque), in grey when asked (Rec. 601 luma). +fn over_white(rgba: &mut [u8], grayscale: bool) { + for px in rgba.as_chunks_mut::<4>().0 { + let gap = 255 - u32::from(px[3]); + let (r, g, b) = ((u32::from(px[0]) + gap).min(255), (u32::from(px[1]) + gap).min(255), (u32::from(px[2]) + gap).min(255)); + let (r, g, b) = if grayscale { + let y = ((299 * r + 587 * g + 114 * b) / 1000).min(255); + (y, y, y) + } else { + (r, g, b) + }; + px[0] = r as u8; + px[1] = g as u8; + px[2] = b as u8; + px[3] = 255; + } +} diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 0677485c91..b803e98b00 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4975,3 +4975,44 @@ fn copy_image_finds_a_picture_inside_form_artwork_under_text() { assert_eq!(d.page_image_copy_file(0, 0).unwrap(), ("jpg", jpeg)); assert!(d.page_image_copy_file(0, 1).is_err()); } + +/// Print as image (Acrobat's Advanced Print Setup): the print-ready PDF carries each page as a +/// picture, the size of the page, with no text left to search; in grayscale the pictures are +/// grey. A page used on several sheets is rendered once. +#[test] +fn printing_as_image_sends_pictures_instead_of_pages() { + let (s, id) = session_with(3); + let page_size = (s.get(id).unwrap().info.pages[1].width as f64, s.get(id).unwrap().info.pages[1].height as f64); + let settings = print::Settings { + pages: vec![1, 2, 1], + layout: print::Layout::Size(print::SizeMode::Actual), + raster: Some(print::Raster { dpi: 100.0, grayscale: false }), + ..Default::default() + }; + let bytes = s.print_pdf(id, &settings).unwrap(); + let mut s2 = Session::new(); + let id2 = s2.open("print.pdf", None, Arc::new(bytes.clone()), None).unwrap(); + assert_eq!(s2.get(id2).unwrap().info.pages.len(), 3, "three sheets, one page each"); + assert!(page_texts(&s2, id2).iter().all(|t| t.trim().is_empty()), "pictures carry no text"); + let doc = pdfcraft_cos::Document::open(Arc::new(bytes)).unwrap(); + let images: Vec = doc + .scan_objects() + .filter_map(|(_, o)| match &*o { + pdfcraft_cos::Object::Stream(st) if st.dict.name(b"Subtype") == Some(b"Image") => Some(st.dict.clone()), + _ => None, + }) + .collect(); + assert_eq!(images.len(), 2, "two distinct pages, each rendered once: {images:?}"); + // 100 dpi: the picture's pixels match the page size. + let w = images.iter().map(|d| d.int(b"Width").unwrap_or(0)).max().unwrap(); + assert!((w as f64 - page_size.0 * 100.0 / 72.0).abs() <= 1.0, "{w} px for {} pt", page_size.0); + assert!(images.iter().all(|d| d.name(b"ColorSpace") == Some(b"DeviceRGB")), "{images:?}"); + // Grayscale renders the pictures grey (the printer is not merely asked for monochrome). + let grey = s.print_pdf(id, &print::Settings { raster: Some(print::Raster { dpi: 72.0, grayscale: true }), ..settings.clone() }).unwrap(); + let mut s3 = Session::new(); + let id3 = s3.open("grey.pdf", None, Arc::new(grey), None).unwrap(); + let mut ex = crate::export::Exporter::new(s3.get(id3).unwrap()); + let png = ex.png(0, 36.0).unwrap(); + let img = image::load_from_memory(&png).unwrap().to_rgba8(); + assert!(img.pixels().all(|p| p[0] == p[1] && p[1] == p[2]), "every pixel grey"); +} diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs index e45babadb5..0de9f4fc37 100644 --- a/crates/print/src/lib.rs +++ b/crates/print/src/lib.rs @@ -228,11 +228,37 @@ pub struct Settings { pub orientation: Orientation, pub layout: Layout, pub content: Content, + /// Print as image (Acrobat's Advanced Print Setup): every page rendered to a picture at this + /// resolution before it is placed on the paper. The engine does the rendering; `impose` is + /// then given the image-only document. + pub raster: Option, +} + +/// How pages are rendered when printing as image. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Raster { + /// Dots per inch, 72–600 (Acrobat offers 150, 300 and 600). + pub dpi: f64, + /// Render the pictures in grey, so the print-ready PDF itself is grayscale. + pub grayscale: bool, +} + +impl Default for Raster { + fn default() -> Self { + Raster { dpi: 300.0, grayscale: false } + } } impl Default for Settings { fn default() -> Self { - Settings { pages: Vec::new(), paper: PAPERS[0].1, orientation: Orientation::Auto, layout: Layout::default(), content: Content::default() } + Settings { + pages: Vec::new(), + paper: PAPERS[0].1, + orientation: Orientation::Auto, + layout: Layout::default(), + content: Content::default(), + raster: None, + } } } diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index f3964f2e50..50e270722a 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2408,3 +2408,6 @@ animation effects Off معطّلة New file only الملف الجديد فقط Old file: {name} الملف القديم: {name} New file: {name} الملف الجديد: {name} + Print as image الطباعة كصورة + Resolution: الدقة: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. استخدمه عندما تتجاهل الطابعة النص أو التظليل أو الشفافية. لا تعود المطبوعة نصًا قابلًا للبحث؛ ومع الطباعة بتدرج الرمادي تكون الصور رمادية. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 08d0d2cab4..850fc6269f 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2350,3 +2350,6 @@ animation effects Off Изключени New file only Само новият файл Old file: {name} Стар файл: {name} New file: {name} Нов файл: {name} + Print as image Печат като изображение + Resolution: Разделителна способност: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Използвайте го, когато принтерът пропуска текст, сенки или прозрачност. Отпечатаното вече не е текст за търсене; с печат в сиво изображенията са сиви. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index fd07b2f9f4..41de4a2cf1 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2461,3 +2461,6 @@ animation effects Off Vypnuto New file only Jen nový soubor Old file: {name} Starý soubor: {name} New file: {name} Nový soubor: {name} + Print as image Tisknout jako obrázek + Resolution: Rozlišení: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Použijte, když tiskárna vynechává text, stínování nebo průhlednost. Výtisk už není prohledávatelný text; s tiskem v odstínech šedi jsou obrázky šedé. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2a4f790853..4a36d792ba 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2388,3 +2388,6 @@ animation effects Off Aus New file only Nur neue Datei Old file: {name} Alte Datei: {name} New file: {name} Neue Datei: {name} + Print as image Als Bild drucken + Resolution: Auflösung: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Verwenden, wenn ein Drucker Text, Schattierungen oder Transparenz auslässt. Der Ausdruck ist kein durchsuchbarer Text mehr; mit Graustufendruck sind die Bilder grau. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index f1d8b87d38..1565b771a9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2397,3 +2397,6 @@ animation effects Off Ανενεργά New file only Μόνο το νέο αρχείο Old file: {name} Παλιό αρχείο: {name} New file: {name} Νέο αρχείο: {name} + Print as image Εκτύπωση ως εικόνα + Resolution: Ανάλυση: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Χρησιμοποιήστε το όταν ένας εκτυπωτής παραλείπει κείμενο, σκίαση ή διαφάνεια. Η εκτύπωση δεν είναι πλέον κείμενο με δυνατότητα αναζήτησης· με εκτύπωση σε αποχρώσεις του γκρι, οι εικόνες είναι γκρι. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index df2feb390f..89ff594f0e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2517,3 +2517,6 @@ animation effects Off Desactivados New file only Solo el archivo nuevo Old file: {name} Archivo antiguo: {name} New file: {name} Archivo nuevo: {name} + Print as image Imprimir como imagen + Resolution: Resolución: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Úsalo cuando una impresora omite texto, sombreados o transparencias. La impresión deja de ser texto que se puede buscar; con Imprimir en escala de grises, las imágenes son grises. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f13e724789..233f6bc2fe 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2387,3 +2387,6 @@ animation effects Off Désactivés New file only Nouveau fichier seul Old file: {name} Ancien fichier : {name} New file: {name} Nouveau fichier : {name} + Print as image Imprimer comme image + Resolution: Résolution : + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. À utiliser quand une imprimante saute du texte, des dégradés ou de la transparence. L'impression n'est plus du texte consultable ; avec l'impression en niveaux de gris, les images sont grises. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 2b3d942dac..a2f065a9c6 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2386,3 +2386,6 @@ animation effects Off Ki New file only Csak az új fájl Old file: {name} Régi fájl: {name} New file: {name} Új fájl: {name} + Print as image Nyomtatás képként + Resolution: Felbontás: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Akkor használja, ha a nyomtató kihagy szöveget, árnyékolást vagy átlátszóságot. A nyomat már nem kereshető szöveg; szürkeárnyalatos nyomtatással a képek szürkék. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 6bed842550..e5f161c705 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2396,3 +2396,6 @@ animation effects Off Disattivati New file only Solo il nuovo file Old file: {name} File vecchio: {name} New file: {name} File nuovo: {name} + Print as image Stampa come immagine + Resolution: Risoluzione: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Usalo quando una stampante salta testo, sfumature o trasparenze. La stampa non è più testo ricercabile; con Stampa in scala di grigi le immagini sono grigie. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 83d8e64767..0fe6858036 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2387,3 +2387,6 @@ animation effects Off オフ New file only 新しいファイルのみ Old file: {name} 古いファイル: {name} New file: {name} 新しいファイル: {name} + Print as image 画像として印刷 + Resolution: 解像度: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. プリンターが文字、グラデーション、透明部分を印刷しないときに使います。印刷物は検索できる文字ではなくなります。グレースケール印刷と併用すると画像はグレーになります。 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 02da45449e..ad19a1aeb4 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2385,3 +2385,6 @@ animation effects Off Өшірулі New file only Тек жаңа файл Old file: {name} Ескі файл: {name} New file: {name} Жаңа файл: {name} + Print as image Сурет ретінде басып шығару + Resolution: Ажыратымдылық: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Принтер мәтінді, көлеңкені немесе мөлдірлікті өткізіп жіберсе, қолданыңыз. Басылған құжат енді іздеуге болатын мәтін емес; сұр реңкпен басып шығарғанда суреттер сұр болады. diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index e3d0fca16c..f0835d906d 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2631,3 +2631,6 @@ key Space Tarpas New file only Tik naujas failas Old file: {name} Senas failas: {name} New file: {name} Naujas failas: {name} + Print as image Spausdinti kaip vaizdą + Resolution: Raiška: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Naudokite, kai spausdintuvas praleidžia tekstą, šešėliavimą ar skaidrumą. Atspaudas nebėra ieškomas tekstas; spausdinant pilkais atspalviais vaizdai yra pilki. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 339fff515d..8171881311 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2367,3 +2367,6 @@ animation effects Off Wyłączone New file only Tylko nowy plik Old file: {name} Stary plik: {name} New file: {name} Nowy plik: {name} + Print as image Drukuj jako obraz + Resolution: Rozdzielczość: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Użyj, gdy drukarka pomija tekst, cieniowanie lub przezroczystość. Wydruk nie jest już tekstem do przeszukiwania; przy druku w skali szarości obrazy są szare. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f2652f52f4..c82232e1ae 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2401,3 +2401,6 @@ animation effects Off Desativados New file only Somente o arquivo novo Old file: {name} Arquivo antigo: {name} New file: {name} Arquivo novo: {name} + Print as image Imprimir como imagem + Resolution: Resolução: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Use quando uma impressora omite texto, sombreamento ou transparência. A impressão deixa de ser texto pesquisável; com Imprimir em tons de cinza, as imagens ficam cinza. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 0779819336..328ec8379f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2300,3 +2300,6 @@ animation effects Off Выкл New file only Только новый файл Old file: {name} Старый файл: {name} New file: {name} Новый файл: {name} + Print as image Печать как изображение + Resolution: Разрешение: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Используйте, когда принтер пропускает текст, заливки или прозрачность. Отпечаток больше не текст с поиском; при печати в оттенках серого изображения серые. diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 5bb21ea6f3..fd2b70da2e 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2392,3 +2392,6 @@ animation effects Off Izklopljeni New file only Samo nova datoteka Old file: {name} Stara datoteka: {name} New file: {name} Nova datoteka: {name} + Print as image Natisni kot sliko + Resolution: Ločljivost: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Uporabite, ko tiskalnik izpusti besedilo, senčenje ali prosojnost. Natis ni več iskalno besedilo; s tiskanjem v sivinah so slike sive. diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 76273e9207..3fe502bfe0 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2300,3 +2300,6 @@ animation effects Off ఆఫ్ New file only కొత్త ఫైల్ మాత్రమే Old file: {name} పాత ఫైల్: {name} New file: {name} కొత్త ఫైల్: {name} + Print as image చిత్రంగా ముద్రించు + Resolution: రిజల్యూషన్: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. ప్రింటర్ వచనం, షేడింగ్ లేదా పారదర్శకతను వదిలేసినప్పుడు దీన్ని వాడండి. ముద్రణ ఇక శోధించగల వచనం కాదు; బూడిద రంగులో ముద్రించినప్పుడు చిత్రాలు బూడిద రంగులో ఉంటాయి. diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 05d6015d03..ce68ea61e4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2383,3 +2383,6 @@ animation effects Off Kapalı New file only Yalnızca yeni dosya Old file: {name} Eski dosya: {name} New file: {name} Yeni dosya: {name} + Print as image Resim olarak yazdır + Resolution: Çözünürlük: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Yazıcı metni, gölgelendirmeyi veya saydamlığı atladığında kullanın. Çıktı artık aranabilir metin değildir; gri tonlamalı yazdırmayla resimler gri olur. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 53613bd179..bc93471f70 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2409,3 +2409,6 @@ animation effects Off Вимкнено New file only Лише новий файл Old file: {name} Старий файл: {name} New file: {name} Новий файл: {name} + Print as image Друкувати як зображення + Resolution: Роздільність: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. Використовуйте, коли принтер пропускає текст, заливки або прозорість. Відбиток більше не текст із пошуком; під час друку у відтінках сірого зображення сірі. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ea9c2214c8..9068f16630 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2351,3 +2351,6 @@ animation effects Off 关 New file only 仅新文件 Old file: {name} 旧文件:{name} New file: {name} 新文件:{name} + Print as image 作为图像打印 + Resolution: 分辨率: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. 当打印机漏印文字、渐变或透明效果时使用。打印结果不再是可搜索的文字;配合灰度打印时图像为灰色。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e019740a33..c45d99f6e3 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2341,3 +2341,6 @@ animation effects Off 關 New file only 僅新檔案 Old file: {name} 舊檔案:{name} New file: {name} 新檔案:{name} + Print as image 以影像列印 + Resolution: 解析度: + Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey. 當印表機漏印文字、漸層或透明效果時使用。列印結果不再是可搜尋的文字;搭配灰階列印時影像為灰色。 diff --git a/crates/ui-egui/src/print_ui.rs b/crates/ui-egui/src/print_ui.rs index 65d72d443c..dd233c969c 100644 --- a/crates/ui-egui/src/print_ui.rs +++ b/crates/ui-egui/src/print_ui.rs @@ -67,6 +67,9 @@ pub struct PrintDraft { pub cut_marks: bool, pub orientation: Orientation, pub content: Content, + /// Print as image (Acrobat's Advanced Print Setup) at `dpi`. + pub as_image: bool, + pub dpi: f64, pub paper: usize, /// The sheet shown in the preview (0-based). pub sheet: usize, @@ -111,6 +114,8 @@ impl Default for PrintDraft { cut_marks: true, orientation: Orientation::Auto, content: Content::DocumentAndMarkups, + as_image: false, + dpi: 300.0, paper: 0, sheet: 0, current_page: 0, @@ -156,7 +161,14 @@ impl PrintDraft { Handling::Booklet => Layout::Booklet { subset: self.booklet_subset, binding: self.binding }, Handling::Poster => Layout::Poster { scale: self.poster_scale, overlap: self.overlap, cut_marks: self.cut_marks }, }; - Ok(print::Settings { pages, paper: PAPERS[self.paper.min(PAPERS.len() - 1)].1, orientation: self.orientation, layout, content: self.content }) + Ok(print::Settings { + pages, + paper: PAPERS[self.paper.min(PAPERS.len() - 1)].1, + orientation: self.orientation, + layout, + content: self.content, + raster: self.as_image.then_some(print::Raster { dpi: self.dpi, grayscale: self.grayscale }), + }) } pub fn job(&self, title: &str) -> spool::Job { @@ -909,6 +921,29 @@ pub(crate) fn body( ], 220.0, ); + // Advanced (Acrobat's Advanced Print Setup): print as image. Folded unless it is on. + ui.add_space(6.0); + egui::CollapsingHeader::new(egui::RichText::new(tl!("Advanced").to_uppercase()).font(theme::semibold(10.5)).color(t.text_faint)) + .id_salt("print-advanced") + .default_open(d.as_image) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.add(widgets::checkbox(&mut d.as_image, tl!("Print as image"))); + ui.add_enabled_ui(d.as_image, |ui| { + ui.label(tl!("Resolution:")); + let mut dpi = d.dpi.round() as u32; + combo(ui, "print-dpi", &mut dpi, &[(150, "150 dpi"), (300, "300 dpi"), (600, "600 dpi")], 100.0); + d.dpi = f64::from(dpi); + }); + }); + ui.label( + egui::RichText::new(tl!( + "Use it when a printer skips text, shading or transparency. The print is no longer searchable text; with Print in grayscale, the pictures are grey." + )) + .small() + .color(t.text_muted), + ); + }); }); ui.add_space(12.0); // Preview. diff --git a/crates/ui-egui/tests/print.rs b/crates/ui-egui/tests/print.rs index 739c04ee94..9f8ee9263c 100644 --- a/crates/ui-egui/tests/print.rs +++ b/crates/ui-egui/tests/print.rs @@ -373,3 +373,31 @@ fn a_printer_without_two_sided_printing_points_to_its_properties() { h.run_steps(2); h.get_by_label_contains(note); } + +/// Print as image (Acrobat's Advanced Print Setup) is a choice in the Print dialog's Advanced +/// section: the saved print-ready PDF carries the page as a picture, no text. +#[test] +fn print_as_image_is_an_advanced_choice_that_saves_pictures() { + let mut h = harness(); + assert!(h.state_mut().execute("print.dialog")); + h.run_steps(3); + h.get_by_label("Advanced".to_uppercase().as_str()).click(); + h.run_steps(2); + h.get_by_label("Print as image").click(); + h.run_steps(2); + assert!(h.state().print_draft.as_image); + let out = std::env::temp_dir().join(format!("pdfcraft-print-image-test-{}.pdf", std::process::id())); + h.state_mut().save_override = Some(out.to_string_lossy().into_owned()); + h.state_mut().print_draft.printer = None; + h.run_steps(1); + h.get_by_label("Save as PDF").click(); + h.run_steps(3); + assert_eq!(h.state().dialog, None); + let bytes = std::fs::read(&out).expect("saved"); + let doc = pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).unwrap(); + assert_eq!(pdfcraft_model::pages(&doc).len(), 1); + let pictures = + doc.scan_objects().filter(|(_, o)| matches!(&**o, pdfcraft_cos::Object::Stream(st) if st.dict.name(b"Subtype") == Some(b"Image"))).count(); + assert_eq!(pictures, 1, "the page is one picture"); + let _ = std::fs::remove_file(out); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index fd9aa770db..b7c37ce33f 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -8105,11 +8105,11 @@ title = "Print in grayscale" area = "L" tier = "P1" milestone = "M10" -status = "partial" +status = "shipped" commands = ["print.dialog"] tools = ["doc_print"] -evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list"] -notes = "asks the printer for monochrome (lp -o, and the Windows page settings as the job's); the PDF itself stays in colour" +evidence = ["crates/engine/src/tests.rs::printing_as_image_sends_pictures_instead_of_pages", "crates/ui-egui/tests/print.rs::print_as_image_is_an_advanced_choice_that_saves_pictures", "crates/automation/tests/automation.rs::doc_print_as_image"] +notes = "asks the printer for monochrome (lp -o, and the Windows page settings as the job's); with Print as image the pictures themselves are rendered grey (Rec. 601 luma), so the print-ready PDF is grayscale too. Still missing against Acrobat: a grey conversion of the vector pages without rendering them." [[feature]] id = "print.print-as-image" @@ -8117,11 +8117,11 @@ title = "Print as image" area = "L" tier = "P1" milestone = "M10" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["print.dialog"] +tools = ["doc_print"] +evidence = ["crates/engine/src/tests.rs::printing_as_image_sends_pictures_instead_of_pages", "crates/ui-egui/tests/print.rs::print_as_image_is_an_advanced_choice_that_saves_pictures", "crates/automation/tests/automation.rs::doc_print_as_image"] +notes = "The Print dialog's Advanced section (folded unless on): Print as image at 150, 300 or 600 dpi; doc_print as_image and dpi (72–600). The pages used are rendered once each the way the viewer shows them (comments left out when the print leaves them out), composed over white, made into an image-only document and imposed on the paper like the pages themselves. The print is no longer searchable text. Still missing against Acrobat: per-printer defaults for it." [[feature]] id = "print.page-setup"