mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 21:11:52 +00:00
* Print in grayscale prints on a Windows driver with no grey option (#5806) Windows refused Print in grayscale ("the printer's driver did not retain the requested grayscale; nothing was sent") on drivers that list no grey option the ticket can name: Microsoft Print to PDF, and an HP Photosmart driver whose grey is its own vendor setting. - On Windows, a grayscale job's copy for the spooler is made grey first with Convert colours (all pages, comments and form fields, black kept black, pictures re-encoded where they can be), and the job says so (Job::grey_pages). The driver is still asked for grey where it has it; one without prints the grey pages instead of refusing them. A copy that can't be read goes as it is and asks the driver strictly, as before. - craft-print 970644b7 (storytold/craft-libs#774) adds Job::grey_pages and asks a driver that lists only psk:Grayscale for that, not psk:Monochrome. - Both the Print dialog and doc_print go through spooler_copy, which now takes the job. * craft-print 1146cdc4: craft-libs main with #774 (grey pages) and #773 (the CUPS printer list waits at most 15 s, storytold/pdfcraft#6761)
This commit is contained in:
1 parent
919db9bd51
commit
fe70ecce73
6 files changed
+135
-18
No files matched your search
Generated
+1
-1
@@ -1243,7 +1243,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-print"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=0e4943cb5e7b50bd4eb01e8c580b97603c8f9e3d#0e4943cb5e7b50bd4eb01e8c580b97603c8f9e3d"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c#1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c"
|
||||
dependencies = [
|
||||
"craft-winprint-gdi",
|
||||
"log",
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1
|
||||
# the commit in `craft-i18n.rev`). Already in the tree through craft-ui.
|
||||
craft-i18n = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
||||
# Shared system spooler; document composition remains in pdfcraft-print.
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "0e4943cb5e7b50bd4eb01e8c580b97603c8f9e3d" }
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c" }
|
||||
# Whether a path stored in a document may be opened (GoToR link targets).
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
|
||||
@@ -106,6 +106,8 @@ fn spool_job(
|
||||
duplex,
|
||||
scaling: spool::Scaling::Exact,
|
||||
grayscale: print::driver_needs_grayscale(a.opt_bool("grayscale")?.unwrap_or(false), raster),
|
||||
// Set when the copy sent is made grey (`spooler_copy`, #5806).
|
||||
grey_pages: false,
|
||||
title,
|
||||
options: {
|
||||
let mut options: Vec<(String, String)> = options.into_iter().chain(print::media_option(paper)).collect();
|
||||
@@ -310,8 +312,8 @@ impl Automation {
|
||||
}
|
||||
(None, Some(printer)) => {
|
||||
let printer = asked.as_ref().map_or(printer, |p| p.name.as_str());
|
||||
let job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index, settings.raster, &laid_out)?;
|
||||
let bytes = pdfcraft_engine::spooler::spooler_copy(bytes);
|
||||
let mut job = spool_job(a, printer, self.doc(a)?.name.clone(), duplex, paper_index, settings.raster, &laid_out)?;
|
||||
let bytes = pdfcraft_engine::spooler::spooler_copy(bytes, &mut job);
|
||||
out["job"] = json!(spool::submit(&bytes, &job).map_err(|e| failed(e.to_string()))?);
|
||||
}
|
||||
_ => return Err(bad("pass either path (save the print-ready PDF) or printer (a name from printers, or \"default\")")),
|
||||
|
||||
+123
-10
@@ -6,20 +6,65 @@ use pdfcraft_cos::{SaveOptions, write_full};
|
||||
|
||||
use crate::guard;
|
||||
|
||||
/// The print-ready PDF `bytes` ([`crate::Session::print_pdf`]) as they go to the system spooler.
|
||||
/// On Windows, whose PDF renderer prints an image of more than 2²² pixels from a shrunk copy,
|
||||
/// large images are drawn as tiles ([`pdfcraft_optimize::tile_large_images`], #5131), so that a
|
||||
/// scan prints at the printer's resolution. Elsewhere, with no large image, and when the copy
|
||||
/// can't be read or written, the bytes go as they are: tiling never stops a print.
|
||||
pub fn spooler_copy(bytes: Vec<u8>) -> Vec<u8> {
|
||||
/// The print-ready PDF `bytes` ([`crate::Session::print_pdf`]) as they go to the system spooler
|
||||
/// with `job`. Two things happen on Windows only; elsewhere the bytes go as they are.
|
||||
///
|
||||
/// - A job in grayscale is made grey here first ([`greyed`]), and `job.grey_pages` says so.
|
||||
/// Windows has no colour conversion of its own, and a driver that lists no grey option (Microsoft
|
||||
/// Print to PDF, or one with only a vendor's own grey setting) refused the job outright
|
||||
/// (#5806). The driver is still asked for grey where it has it.
|
||||
/// - Large images are drawn as tiles ([`pdfcraft_optimize::tile_large_images`], #5131): Windows'
|
||||
/// PDF renderer prints an image of more than 2²² pixels from a shrunk copy, so a scan comes out
|
||||
/// at the printer's resolution.
|
||||
///
|
||||
/// When the copy can't be read or written, the bytes go as they are: neither step ever stops a
|
||||
/// print, and a grayscale job whose pages couldn't be made grey keeps asking the driver for grey,
|
||||
/// strictly, as before.
|
||||
pub fn spooler_copy(bytes: Vec<u8>, job: &mut crate::print::spool::Job) -> Vec<u8> {
|
||||
if !cfg!(windows) {
|
||||
return bytes;
|
||||
}
|
||||
let bytes = Arc::new(bytes);
|
||||
let mut bytes = Arc::new(bytes);
|
||||
if job.grayscale {
|
||||
match greyed(bytes.clone()) {
|
||||
Some(Some(grey)) => {
|
||||
bytes = Arc::new(grey);
|
||||
job.grey_pages = true;
|
||||
}
|
||||
Some(None) => job.grey_pages = true,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
// Once `tiled` returns, its document is gone and `bytes` is the only owner left: no copy.
|
||||
tiled(bytes.clone()).unwrap_or_else(|| Arc::try_unwrap(bytes).unwrap_or_else(|shared| (*shared).clone()))
|
||||
}
|
||||
|
||||
/// `bytes` with every colour on every page, its comments and form fields included, turned grey by
|
||||
/// Convert colours (black kept black, pictures re-encoded where they can be): `Some(Some(_))`.
|
||||
/// `Some(None)` when nothing needed converting, and `None` when the copy can't be read or written.
|
||||
///
|
||||
/// Convert colours leaves shadings, patterns, inline images and pictures it can't re-encode
|
||||
/// (indexed, CMYK, JPEG 2000 …) as they are; the driver is still asked for grey, so a driver with a
|
||||
/// grey option makes those grey too, and one without prints them as they are.
|
||||
pub fn greyed(bytes: Arc<Vec<u8>>) -> Option<Option<Vec<u8>>> {
|
||||
guard(|| {
|
||||
let mut doc = pdfcraft_cos::Document::open(bytes).ok()?;
|
||||
let pages: Vec<usize> = (0..pdfcraft_model::pages(&doc).len()).collect();
|
||||
let settings =
|
||||
pdfcraft_edit::ConvertColours { target: pdfcraft_edit::ColourTarget::Gray, preserve_black: true, images: true, annotations: true };
|
||||
let mut recolour = |doc: &pdfcraft_cos::Document, s: &pdfcraft_cos::Stream, _: pdfcraft_edit::ColourTarget| {
|
||||
pdfcraft_optimize::recolour_image(doc, s, pdfcraft_optimize::ImageTarget::Gray)
|
||||
};
|
||||
let done = pdfcraft_edit::convert_colours(&mut doc, &pages, &settings, &mut recolour).ok()?;
|
||||
if done.colours == 0 && done.images == 0 {
|
||||
return Some(None);
|
||||
}
|
||||
write_full(&doc, &SaveOptions::default()).ok().map(Some)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// `bytes` with their large images drawn as tiles, or `None` when there is none, or the copy
|
||||
/// can't be read or written.
|
||||
pub fn tiled(bytes: Arc<Vec<u8>>) -> Option<Vec<u8>> {
|
||||
@@ -37,6 +82,7 @@ mod tests {
|
||||
use pdfcraft_cos::{Dict, Document, Object, Stream};
|
||||
|
||||
use super::*;
|
||||
use crate::print::spool::Job;
|
||||
|
||||
/// A one-page PDF drawing a `w` × `h` grey image over the page.
|
||||
fn page_with_image(w: i64, h: i64) -> Vec<u8> {
|
||||
@@ -90,16 +136,83 @@ mod tests {
|
||||
fn a_print_with_nothing_to_tile_or_unreadable_goes_as_it_is() {
|
||||
assert_eq!(tiled(Arc::new(page_with_image(100, 100))), None);
|
||||
assert_eq!(tiled(Arc::new(b"not a PDF".to_vec())), None);
|
||||
assert_eq!(spooler_copy(b"not a PDF".to_vec()), b"not a PDF");
|
||||
let mut job = Job::default();
|
||||
assert_eq!(spooler_copy(b"not a PDF".to_vec(), &mut job), b"not a PDF");
|
||||
let small = page_with_image(100, 100);
|
||||
assert_eq!(spooler_copy(small.clone()), small);
|
||||
assert_eq!(spooler_copy(small.clone(), &mut job), small);
|
||||
assert!(!job.grey_pages);
|
||||
// A grayscale job whose copy can't be read asks the driver for grey as before.
|
||||
let mut grey = Job { grayscale: true, ..Job::default() };
|
||||
assert_eq!(spooler_copy(b"not a PDF".to_vec(), &mut grey), b"not a PDF");
|
||||
assert!(!grey.grey_pages);
|
||||
}
|
||||
|
||||
/// A one-page PDF filling the page in red.
|
||||
fn red_page() -> Vec<u8> {
|
||||
let mut doc = Document::new_empty();
|
||||
let pages = doc.root().and_then(|r| doc.get(r).as_dict().and_then(|d| d.reference(b"Pages"))).unwrap();
|
||||
let content = doc.add(Object::Stream(Stream::from_raw(Dict::new(), b"1 0 0 rg 0 0 612 792 re f".to_vec())));
|
||||
let mut page = Dict::new();
|
||||
page.set("Type", Object::name("Page"));
|
||||
page.set("Parent", Object::Ref(pages));
|
||||
page.set("MediaBox", Object::Array([0, 0, 612, 792].map(Object::Int).to_vec()));
|
||||
page.set("Contents", Object::Ref(content));
|
||||
let page = doc.add(Object::Dict(page));
|
||||
doc.update_dict(pages, |d| {
|
||||
d.set("Count", Object::Int(1));
|
||||
d.set("Kids", Object::Array(vec![Object::Ref(page)]));
|
||||
})
|
||||
.unwrap();
|
||||
write_full(&doc, &SaveOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
/// The operators of the first page's content.
|
||||
fn first_page_content(bytes: Vec<u8>) -> String {
|
||||
let doc = Document::open(Arc::new(bytes)).unwrap();
|
||||
let page = pdfcraft_model::pages(&doc)[0].obj;
|
||||
let contents = doc.get(page).as_dict().and_then(|d| d.reference(b"Contents")).unwrap();
|
||||
match &*doc.get(contents) {
|
||||
Object::Stream(s) => String::from_utf8_lossy(&s.decoded().unwrap()).into_owned(),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// #5806: a page in colour is made grey for a grayscale job, which says so.
|
||||
#[test]
|
||||
fn a_colour_page_is_made_grey_for_a_grayscale_job() {
|
||||
let grey = greyed(Arc::new(red_page())).expect("readable").expect("converted");
|
||||
let content = first_page_content(grey.clone());
|
||||
assert!(!content.contains(" rg"), "{content}");
|
||||
assert!(content.contains(" g"), "{content}");
|
||||
// Already grey: nothing to convert, and the pages are still grey.
|
||||
assert_eq!(greyed(Arc::new(grey)), Some(None));
|
||||
assert_eq!(greyed(Arc::new(b"not a PDF".to_vec())), None);
|
||||
}
|
||||
|
||||
/// On Windows a grayscale job's copy is grey and the job says its pages are; elsewhere the
|
||||
/// bytes go as they are and the driver is asked, as before. A colour job is never converted.
|
||||
#[test]
|
||||
fn a_grayscale_job_sends_grey_pages_on_windows_only() {
|
||||
let red = red_page();
|
||||
let mut job = Job { grayscale: true, ..Job::default() };
|
||||
let sent = spooler_copy(red.clone(), &mut job);
|
||||
if cfg!(windows) {
|
||||
assert!(job.grey_pages);
|
||||
assert!(!first_page_content(sent).contains(" rg"));
|
||||
} else {
|
||||
assert!(!job.grey_pages);
|
||||
assert_eq!(sent, red);
|
||||
}
|
||||
let mut colour = Job::default();
|
||||
assert_eq!(spooler_copy(red.clone(), &mut colour), red);
|
||||
assert!(!colour.grey_pages);
|
||||
}
|
||||
|
||||
/// On Windows the spooler gets the tiled copy ([`tiled`]); elsewhere the bytes as they are.
|
||||
#[test]
|
||||
fn the_spooler_gets_tiles_on_windows_only() {
|
||||
let large = page_with_image(2100, 2100);
|
||||
let sent = spooler_copy(large.clone());
|
||||
let sent = spooler_copy(large.clone(), &mut Job::default());
|
||||
if cfg!(windows) {
|
||||
assert_eq!(Some(&sent), tiled(Arc::new(large.clone())).as_ref());
|
||||
assert_ne!(sent, large);
|
||||
|
||||
@@ -557,7 +557,7 @@ impl PdfCraftApp {
|
||||
/// the progress card counts the sheets as they reach the spooler, and the spooler's answer
|
||||
/// arrives as a notice.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn send_to_printer(&mut self, printer: String, bytes: Vec<u8>, job: spool::Job) -> bool {
|
||||
fn send_to_printer(&mut self, printer: String, bytes: Vec<u8>, mut job: spool::Job) -> bool {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let ctx = self.ctx.clone();
|
||||
let spooler = self.spooler.clone();
|
||||
@@ -567,9 +567,9 @@ impl PdfCraftApp {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
};
|
||||
// Large pictures become tiles on Windows (#5131): work on the whole file, kept off the
|
||||
// UI thread with the rest.
|
||||
let bytes = pdfcraft_engine::spooler::spooler_copy(bytes);
|
||||
// On Windows grayscale makes the pages grey (#5806) and large pictures become tiles
|
||||
// (#5131): work on the whole file, kept off the UI thread with the rest.
|
||||
let bytes = pdfcraft_engine::spooler::spooler_copy(bytes, &mut job);
|
||||
// The receiver may be gone (the app quit): nothing to report to then.
|
||||
let result = match &spooler {
|
||||
Some(submit) => submit(&bytes, &job),
|
||||
|
||||
@@ -341,6 +341,8 @@ impl PrintDraft {
|
||||
},
|
||||
scaling: spool::Scaling::Exact,
|
||||
grayscale: print::driver_needs_grayscale(self.grayscale, self.raster()),
|
||||
// Set when the copy sent is made grey (`spooler_copy`, #5806).
|
||||
grey_pages: false,
|
||||
title: title.to_string(),
|
||||
// Only for the printer the options were read from.
|
||||
options: match (&self.printer, &self.driver_options) {
|
||||
|
||||
Reference in new issue
Block a user