Print: print as image (Advanced), with grayscale pictures (#1965)

* Print: print as image (Advanced), with grayscale pictures

print::Settings.raster (dpi 72-600, grayscale): Session::print_pdf renders each
distinct page used once the way the viewer shows it (comments left out when the
print leaves them out), composes it over white, grey by Rec. 601 luma when
grayscale is on, makes an image-only document of the pictures and imposes it on
the paper like the pages themselves. The Print dialog's new Advanced section
(folded unless on) offers it at 150, 300 or 600 dpi; doc_print takes as_image
and dpi. Rows in every language; parity print.print-as-image shipped,
print.grayscale shipped (the pictures are grey, not only the printer asked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ATTRIBUTION.toml: the language files' checksums after the new rows

cargo xtask assets checks each catalog's sha256; the rows this branch adds
changed the files without updating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refresh catalog hashes after merging main

---------

Co-authored-by: Osvaldo Duran <338149330+regendorf-ux@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
regendorf-ux
2026-10-15 02:34:09 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Osvaldo Duran Brandon Thomas
parent 57256f9a4c
commit 8e803b6290
32 changed files with 340 additions and 32 deletions
+21 -21
View File
@@ -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"
+12 -1
View File
@@ -125,7 +125,18 @@ impl Automation {
bad(format!("unknown paper {p:?}; choose one of {}", PAPERS.iter().map(|(name, _)| *name).collect::<Vec<_>>().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)?;
+2
View File
@@ -512,6 +512,8 @@ pub fn tools() -> Vec<ToolDef> {
"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"],
+20
View File
@@ -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:?}"),
}
}
+21
View File
@@ -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<usize> = 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 => {
+61
View File
@@ -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<Vec<u8>, 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<u8>)> = Vec::with_capacity(pages.len());
let mut used: Option<f64> = 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;
}
}
+41
View File
@@ -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<pdfcraft_cos::Dict> = 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");
}
+27 -1
View File
@@ -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<Raster>,
}
/// 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,
}
}
}
+3
View File
@@ -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. استخدمه عندما تتجاهل الطابعة النص أو التظليل أو الشفافية. لا تعود المطبوعة نصًا قابلًا للبحث؛ ومع الطباعة بتدرج الرمادي تكون الصور رمادية.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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. Използвайте го, когато принтерът пропуска текст, сенки или прозрачност. Отпечатаното вече не е текст за търсене; с печат в сиво изображенията са сиви.
Can't render this file because it contains an unexpected character in line 8 and column 16.
+3
View File
@@ -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é.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 51.
+3
View File
@@ -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. Χρησιμοποιήστε το όταν ένας εκτυπωτής παραλείπει κείμενο, σκίαση ή διαφάνεια. Η εκτύπωση δεν είναι πλέον κείμενο με δυνατότητα αναζήτησης· με εκτύπωση σε αποχρώσεις του γκρι, οι εικόνες είναι γκρι.
Can't render this file because it contains an unexpected character in line 475 and column 9.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 60.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 39.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 7 and column 44.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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. プリンターが文字、グラデーション、透明部分を印刷しないときに使います。印刷物は検索できる文字ではなくなります。グレースケール印刷と併用すると画像はグレーになります。
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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. Принтер мәтінді, көлеңкені немесе мөлдірлікті өткізіп жіберсе, қолданыңыз. Басылған құжат енді іздеуге болатын мәтін емес; сұр реңкпен басып шығарғанда суреттер сұр болады.
Can't render this file because it contains an unexpected character in line 822 and column 9.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 401 and column 9.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -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. Используйте, когда принтер пропускает текст, заливки или прозрачность. Отпечаток больше не текст с поиском; при печати в оттенках серого изображения серые.
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -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. ప్రింటర్ వచనం, షేడింగ్ లేదా పారదర్శకతను వదిలేసినప్పుడు దీన్ని వాడండి. ముద్రణ ఇక శోధించగల వచనం కాదు; బూడిద రంగులో ముద్రించినప్పుడు చిత్రాలు బూడిద రంగులో ఉంటాయి.
Can't render this file because it contains an unexpected character in line 13 and column 16.
+3
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 821 and column 9.
+3
View File
@@ -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. Використовуйте, коли принтер пропускає текст, заливки або прозорість. Відбиток більше не текст із пошуком; під час друку у відтінках сірого зображення сірі.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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. 当打印机漏印文字、渐变或透明效果时使用。打印结果不再是可搜索的文字;配合灰度打印时图像为灰色。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3
View File
@@ -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. 當印表機漏印文字、漸層或透明效果時使用。列印結果不再是可搜尋的文字;搭配灰階列印時影像為灰色。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+36 -1
View File
@@ -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.
+28
View File
@@ -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);
}
+8 -8
View File
@@ -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"