mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Print: Windows lists its printers and spools jobs (#847)
* Print: Windows lists its printers and spools jobs (#756)
The Print dialog's printer list comes from Win32_Printer through PowerShell's
CIM (wmic is gone from Windows 11), and jobs are spooled by the embedded
windows_print.ps1: the in-box Windows.Data.Pdf renders each sheet at the
printer's own resolution and System.Drawing.Printing carries copies,
collation, duplex and colour as the job's settings. Both run through
powershell, so the Rust code touches no Win32 API. Fixes #756, with the
printer-not-showing duplicates #160, #244, #293, #465 and #644; CIM approach
from #452. On CI a paused Generic / Text Only test printer catches the job in
its queue; a parser test keeps the script valid PowerShell.
* Print: fix the Windows await helper and render the job on CI (dry run)
Real-machine testing found [System.WindowsRuntimeSystemExtensions]::GetMethods()
failing with 'does not contain a method named GetMethods': GetMethods is an
instance method of [Type], so it is called with '.', the form the canonical
WinRT-await snippets use, not '::'. The first CI run missed it because the
end-to-end test skipped silently when the runner refused the test printer, so
the script had never actually run.
The script now honours PDFCRAFT_PRINT_DRYRUN=1: render every sheet with
Windows.Data.Pdf to PNGs in the temporary folder and print nothing. The test
runs that half unconditionally, so CI exercises the whole WinRT path —
projection, await helpers, render — on any machine, while the paused-printer
half stays best-effort and prints why it skipped. Win32_Printer enumeration
asks only for Name and Default, so a machine with nine printers waits less.
* Tests: the find-options wait in the viewer test is a deadline too
4280f83 converted the frame-count waits in this test to deadlines after the
same 12-of-15 race under a full workspace test run, but left the first wait —
the one for the 'page' matches — on the old 60-frame loop, and it flaked the
same way on PR #847's CI. Same fix: wait up to 20 s for the 15 matches, then
assert.
This commit is contained in:
1 parent
ae4f5d44f3
commit
33c14e07cf
11 files changed
+430
-35
No files matched your search
@@ -145,6 +145,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
- **2026-10-10 (M9, #179 follow-up):** A Windows store certificate that can't sign is no longer left out of the Sign dialog silently: `windows::list` names it with the reason and Windows error code (no private key, a P-521/Brainpool/Ed25519 key, a key CNG can't open), the picker shows it greyed and unselectable, and `sign_windows_ids` returns it under `unusable`. A key that needs a permission prompt to open is listed instead of skipped. Reported by a user whose CA-issued identity showed in Acrobat but not here; a plain .p12 import into the CNG and legacy Microsoft providers was verified to list, so the next report will carry the reason. Store test adds a key-less certificate; overall estimate unchanged (about 30–35%).
|
||||
- **2026-10-10 (M10.2, JPEG scanning follow-up to #729):** Skip marker fill bytes and keep reading JFIF, APP14 and ICC metadata after the frame header, stopping before scan data or at EOI. Invalid segment lengths return errors; damaged or mismatched ICC profiles retain #729's device-space fallback. Synthetic regression cases added but not run at the contributor's request. Estimates unchanged.
|
||||
- **2026-10-10 (M13, desktop action outputs):** Desktop Action Wizard keeps every result when input basenames repeat, or when the destination already has a result from an earlier run. It preserves the original filename and extension for a free name and adds numbered suffixes for occupied names; the bounded search returns an error rather than reusing an occupied path. A real-shell regression drives Start twice and reopens the three-page and one-page results, checking that the original inputs and earlier output remain intact. This covers the desktop writer separately from the CLI/MCP action_run repair in #877. Milestone and effort estimates unchanged.
|
||||
- **2026-10-10 (L Print, #756):** Windows prints. The Print dialog lists the system's printers and its default from `Win32_Printer` (through PowerShell's CIM; `wmic` is gone from Windows 11), and Print spools the print-ready PDF: the in-box Windows.Data.Pdf renders each sheet at the printer's own resolution and System.Drawing.Printing carries copies, collation, duplex and colour as the job's settings — both reached through `powershell`, so the Rust code touches no Win32 API and the machine needs no PDF reader. This closes #756 and the "printer not showing" duplicates #160, #244, #293, #465 and #644, using the CIM approach community-tested in #452; the tests render the job's sheets through the whole WinRT path on CI (dry run, no printer needed) and, where a printer can be added, catch a real job in its paused queue. Web printing is still missing; milestone and effort estimates are unchanged.
|
||||
|
||||
- **2026-10-10 (M3, #740):** Column select, as in Acrobat: holding Alt (Option on macOS) when a drag starts selects only the text inside the rectangle, with the Select tool, the text markup tools and Redact; Edit ▸ Column select does it with a plain drag, for Linux desktops whose window manager takes Alt-drag (Xfce, Cinnamon, MATE, KDE Plasma 5 by default). Copy gives rows top to bottom with tab-separated cells (right to left in right-to-left rows, reading order for vertical text); `text_extract` takes the same rectangle as `rect`. The new label is in all 15 catalogs. Also repaired `parity/acrobat-features.toml`, which #650 had left unparseable (duplicate keys on `form.field-list`). Still missing: column selection across pages and ⇧-click extension. Estimates unchanged.
|
||||
- **2026-10-10 (progress docs, hours per audience):** Each readiness number gets its own hours to ≈ 95%: full ≈ 49% / 780–1,400 h, mainstream ≈ 55% / 500–900 h, essentials ≈ 66% / 180–330 h. The full number is now the standard's additive weighted sum over the dimensions (written weights), 47% → 49%: method aligned with the standard, no new evidence. Beta ≈ 26 points, ≈ 400–700 h.
|
||||
|
||||
+11
-7
@@ -32,11 +32,15 @@ Each source page becomes a Form XObject (its content wrapped in q/Q, plus the pr
|
||||
annotations); sheets place them with a clip. The result is a fresh, unencrypted,
|
||||
garbage-collected file (callers check the print permission).
|
||||
|
||||
`spool` talks to CUPS: printers come from `lpstat -p -d` (the queues) and `lpstat -e` (driverless
|
||||
destinations no queue exists for; CUPS builds a temporary one when the job arrives), jobs go to
|
||||
`lp` with copies, collation, duplex and monochrome options. The job is piped to `lp` on stdin and
|
||||
never written to a temp file, where another local user could read or swap it. Other platforms
|
||||
report that printing to a printer isn't available yet; the print-ready PDF can always be saved.
|
||||
`spool` talks to CUPS on macOS and Linux: printers come from `lpstat -p -d` (the queues) and
|
||||
`lpstat -e` (driverless destinations no queue exists for; CUPS builds a temporary one when the
|
||||
job arrives), jobs go to `lp` with copies, collation, duplex and monochrome options. The job is
|
||||
piped to `lp` on stdin and never written to a temp file, where another local user could read or
|
||||
swap it. On Windows the printers come from `Win32_Printer` and jobs are rendered by the in-box
|
||||
`Windows.Data.Pdf` and spooled by `System.Drawing.Printing` with the driver's own settings (both
|
||||
through `powershell`, so no Win32 API is called here); that job file exists for the seconds the
|
||||
spooler reads it, under a fresh unpredictable name. Other platforms report that printing to a
|
||||
printer isn't available yet; the print-ready PDF can always be saved.
|
||||
|
||||
Not yet: Windows and web spoolers, print as image, poster labels, PostScript output, colour
|
||||
conversion for grayscale.
|
||||
Not yet: the web spooler, print as image, poster labels, PostScript output, colour conversion for
|
||||
grayscale.
|
||||
+136
-4
@@ -1,7 +1,10 @@
|
||||
//! The system print spooler. On macOS and Linux this is CUPS: printers come from `lpstat` — its
|
||||
//! queues, and the driverless destinations it can print to without one — and jobs are piped to
|
||||
//! `lp` with the job options (copies, collation, duplex, colour). Other platforms report that
|
||||
//! printing isn't available yet; the print-ready PDF can still be saved.
|
||||
//! `lp` with the job options (copies, collation, duplex, colour). On Windows the printers come
|
||||
//! from `Win32_Printer` and jobs are rendered with the in-box `Windows.Data.Pdf` and spooled
|
||||
//! with the driver's own settings — both through `powershell`, so no Win32 API is called here.
|
||||
//! Other platforms report that printing isn't available yet; the print-ready PDF can still be
|
||||
//! saved.
|
||||
|
||||
use crate::PrintError;
|
||||
|
||||
@@ -309,7 +312,18 @@ pub fn printers() -> Vec<Printer> {
|
||||
let available = lpstat_e_command().output().ok().map(|o| String::from_utf8_lossy(&o.stdout).into_owned());
|
||||
printers_parsed(&queues, available.as_deref())
|
||||
}
|
||||
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Win32_Printer through CIM: `wmic` was removed from Windows 11 (24H2) and
|
||||
// `Get-CimInstance` is on every desktop install. A machine whose PowerShell is locked
|
||||
// down lists no printers and the dialog offers only Save as PDF, as with CUPS that
|
||||
// isn't running.
|
||||
match powershell_command(POWERSHELL_PRINTERS).output() {
|
||||
Ok(o) if o.status.success() => parse_windows_printers(&String::from_utf8_lossy(&o.stdout)),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(windows, all(unix, not(target_arch = "wasm32")))))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
@@ -333,19 +347,137 @@ pub fn printers_parsed(queues: &str, available: Option<&str>) -> Vec<Printer> {
|
||||
printers
|
||||
}
|
||||
|
||||
/// The `powershell` command that runs a script. `-NoProfile` keeps a user's profile from
|
||||
/// writing banners into what is parsed, `-NonInteractive` from waiting for input.
|
||||
#[cfg(windows)]
|
||||
fn powershell_command(script: &str) -> std::process::Command {
|
||||
let mut c = std::process::Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", script]);
|
||||
c
|
||||
}
|
||||
|
||||
/// The Windows printer list: one line per printer, `*` on the system default. UTF-8, through
|
||||
/// the output encoding the script sets first. Only the two properties the list needs are
|
||||
/// pulled, so a machine with many printers doesn't wait for the rest of each record.
|
||||
#[cfg(windows)]
|
||||
const POWERSHELL_PRINTERS: &str = "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); Get-CimInstance Win32_Printer -Property Name, Default | ForEach-Object { if ($_.Default) { '*' + $_.Name } else { $_.Name } }";
|
||||
|
||||
/// The Print dialog's printers from the Windows printer list: one per line, `*` before the
|
||||
/// system default (a Windows queue name may not contain `*`). The first `*` wins when a broken
|
||||
/// spooler marks more than one, so the dialog still has a single default to preselect.
|
||||
pub fn parse_windows_printers(out: &str) -> Vec<Printer> {
|
||||
let mut printers: Vec<Printer> = out
|
||||
.lines()
|
||||
.map(|l| l.strip_suffix('\r').unwrap_or(l))
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| match l.strip_prefix('*') {
|
||||
Some(name) => Printer { name: name.to_string(), default: true },
|
||||
None => Printer { name: l.to_string(), default: false },
|
||||
})
|
||||
.collect();
|
||||
let mut seen_default = false;
|
||||
for p in &mut printers {
|
||||
if p.default {
|
||||
p.default = !seen_default;
|
||||
seen_default = true;
|
||||
}
|
||||
}
|
||||
printers
|
||||
}
|
||||
|
||||
/// Send a print-ready PDF to the spooler. Returns the spooler's message (the job id).
|
||||
pub fn submit(pdf: &[u8], job: &Job) -> Result<String, PrintError> {
|
||||
#[cfg(all(unix, not(target_arch = "wasm32")))]
|
||||
{
|
||||
submit_via(std::process::Command::new("lp"), pdf, job)
|
||||
}
|
||||
#[cfg(not(all(unix, not(target_arch = "wasm32"))))]
|
||||
#[cfg(windows)]
|
||||
{
|
||||
windows_submit(pdf, job, &[])
|
||||
}
|
||||
#[cfg(not(any(windows, all(unix, not(target_arch = "wasm32")))))]
|
||||
{
|
||||
let _ = (pdf, job);
|
||||
Err(PrintError::Spool("printing to a printer isn't available on this platform yet; save the print-ready PDF instead".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// [`submit`] on Windows: the print-ready PDF goes to a job file of its own (WinRT reads from
|
||||
/// a path), the embedded [`WINDOWS_PRINT_SCRIPT`] spools it, and the file is gone again however
|
||||
/// the job ended. `extra_envs` reaches the script too — the tests use it for
|
||||
/// `PDFCRAFT_PRINT_DRYRUN`, which renders without a printer.
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn windows_submit(pdf: &[u8], job: &Job, extra_envs: &[(&str, &str)]) -> Result<String, PrintError> {
|
||||
let path = windows_job_file(pdf)?;
|
||||
let mut ps = powershell_command(WINDOWS_PRINT_SCRIPT);
|
||||
for (key, value) in windows_env_vars(&path, job).into_iter().chain(extra_envs.iter().map(|(k, v)| (k.to_string(), v.to_string()))) {
|
||||
ps.env(key, value);
|
||||
}
|
||||
let out = ps.output();
|
||||
// Remove the job file before reporting: a spooler that refused the job must not leave the
|
||||
// document behind.
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let out = out.map_err(|e| PrintError::Spool(format!("the print spooler is not available: {e}")))?;
|
||||
if !out.status.success() {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
let err = if err.is_empty() { String::from_utf8_lossy(&out.stdout).trim().to_string() } else { err };
|
||||
return Err(PrintError::Spool(if err.is_empty() { "the print job was refused".into() } else { err }));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// The job file [`windows_submit`] hands to `powershell`, in this user's own temporary folder:
|
||||
/// a fresh name no other local user can predict (`create_new`, with the process id and the
|
||||
/// time), written before anything is spawned and removed after. WinRT reads a path, so unlike
|
||||
/// the job piped to `lp` this one touches disk for the seconds the spooler reads it.
|
||||
#[cfg(windows)]
|
||||
fn windows_job_file(pdf: &[u8]) -> Result<std::path::PathBuf, PrintError> {
|
||||
use std::io::Write as _;
|
||||
let written = |e: std::io::Error| PrintError::Spool(format!("the print job could not be written: {e}"));
|
||||
let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
|
||||
for attempt in 0..64u32 {
|
||||
let path = std::env::temp_dir().join(format!("pdfcraft-print-{}-{nanos}-{attempt}.pdf", std::process::id()));
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(&path) {
|
||||
Ok(mut file) => {
|
||||
file.write_all(pdf).map_err(written)?;
|
||||
return Ok(path);
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(e) => return Err(written(e)),
|
||||
}
|
||||
}
|
||||
Err(PrintError::Spool("the print job could not be written: no free name in the temporary folder".into()))
|
||||
}
|
||||
|
||||
/// The print script's environment: no arguments, so a printer name with quotes or spaces
|
||||
/// survives the command line and the document's path is not visible in the process list while
|
||||
/// it prints. The driver options in [`Job::options`] are CUPS' PPD keywords; on Windows the
|
||||
/// driver's own per-printer preferences apply (Print ▸ Properties…), so they are not sent.
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
pub(crate) fn windows_env_vars(path: &std::path::Path, job: &Job) -> Vec<(String, String)> {
|
||||
vec![
|
||||
("PDFCRAFT_PRINT_FILE".into(), path.to_string_lossy().into_owned()),
|
||||
("PDFCRAFT_PRINT_PRINTER".into(), job.printer.clone().unwrap_or_default()),
|
||||
("PDFCRAFT_PRINT_COPIES".into(), job.copies.to_string()),
|
||||
("PDFCRAFT_PRINT_COLLATE".into(), if job.collate { "1" } else { "0" }.into()),
|
||||
(
|
||||
"PDFCRAFT_PRINT_DUPLEX".into(),
|
||||
match job.duplex {
|
||||
Duplex::Off => "off",
|
||||
Duplex::LongEdge => "long-edge",
|
||||
Duplex::ShortEdge => "short-edge",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
("PDFCRAFT_PRINT_GRAYSCALE".into(), if job.grayscale { "1" } else { "0" }.into()),
|
||||
("PDFCRAFT_PRINT_TITLE".into(), job.title.clone()),
|
||||
]
|
||||
}
|
||||
|
||||
/// The embedded print script (see [`windows_submit`]).
|
||||
#[cfg(windows)]
|
||||
pub(crate) const WINDOWS_PRINT_SCRIPT: &str = include_str!("windows_print.ps1");
|
||||
|
||||
/// [`submit`] with the spooler command given, so tests can stand in for `lp`.
|
||||
///
|
||||
/// The job goes to `lp` on stdin, never through a file: a predictable job folder in a shared
|
||||
|
||||
+120
-1
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::spool::{Duplex, Job, lp_args, parse_lpstat};
|
||||
use crate::spool::{Duplex, Job, Printer, lp_args, parse_lpstat, parse_windows_printers, windows_env_vars};
|
||||
|
||||
/// `n` pages of 200×300 (page 3 is landscape 300×200 when n ≥ 3); page i shows "(Page i+1)".
|
||||
/// Page 1 carries a printable square comment, a non-printing note, and a stamp.
|
||||
@@ -391,6 +391,125 @@ fn the_spoolers_reply_drops_the_file_count_of_a_job_sent_on_stdin() {
|
||||
assert_eq!(spool::job_message(b""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_printers_carry_one_default_and_keep_their_names() {
|
||||
// Windows marks the system default with a `*` no queue name may contain; names hold spaces,
|
||||
// parentheses and non-ASCII letters, and the lines end with CRLF.
|
||||
let printers = parse_windows_printers("*Microsoft Print to PDF\r\nHP LaserJet 400 (Copy 2)\r\nКопир Canon iR\r\n\r\n*XPS via USB\r\n");
|
||||
assert_eq!(printers[0], Printer { name: "Microsoft Print to PDF".into(), default: true });
|
||||
assert_eq!(printers[1], Printer { name: "HP LaserJet 400 (Copy 2)".into(), default: false });
|
||||
assert_eq!(printers[2], Printer { name: "Копир Canon iR".into(), default: false });
|
||||
// A spooler marking two defaults: the first still wins, so the dialog has one to preselect.
|
||||
assert_eq!(printers[3], Printer { name: "XPS via USB".into(), default: false });
|
||||
assert_eq!(parse_windows_printers("\r\n"), Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_windows_job_is_the_environment_of_the_print_script() {
|
||||
let path = std::path::Path::new("C:\\Users\\me\\AppData\\Local\\Temp\\pdfcraft-print-42.pdf");
|
||||
let job = Job {
|
||||
printer: Some("HP \"Laser\" Jet".into()),
|
||||
copies: 3,
|
||||
duplex: Duplex::ShortEdge,
|
||||
grayscale: true,
|
||||
title: "Angebot.pdf".into(),
|
||||
..Job::default()
|
||||
};
|
||||
let vars = windows_env_vars(path, &job);
|
||||
let env = |key: &str| vars.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str()).unwrap();
|
||||
assert_eq!(env("PDFCRAFT_PRINT_FILE"), "C:\\Users\\me\\AppData\\Local\\Temp\\pdfcraft-print-42.pdf");
|
||||
// Quotes and spaces in a printer name survive because nothing goes on the command line.
|
||||
assert_eq!(env("PDFCRAFT_PRINT_PRINTER"), "HP \"Laser\" Jet");
|
||||
assert_eq!(env("PDFCRAFT_PRINT_COPIES"), "3");
|
||||
assert_eq!(env("PDFCRAFT_PRINT_COLLATE"), "1");
|
||||
assert_eq!(env("PDFCRAFT_PRINT_DUPLEX"), "short-edge");
|
||||
assert_eq!(env("PDFCRAFT_PRINT_GRAYSCALE"), "1");
|
||||
assert_eq!(env("PDFCRAFT_PRINT_TITLE"), "Angebot.pdf");
|
||||
// No printer chosen means the system default: an empty name, not the word "default".
|
||||
let one = |j: &Job, key: &str| windows_env_vars(path, j).into_iter().find(|(k, _)| k == key).unwrap().1;
|
||||
assert_eq!(one(&Job::default(), "PDFCRAFT_PRINT_PRINTER"), "");
|
||||
assert_eq!(one(&Job { duplex: Duplex::Off, ..Job::default() }, "PDFCRAFT_PRINT_DUPLEX"), "off");
|
||||
assert_eq!(one(&Job { duplex: Duplex::LongEdge, ..Job::default() }, "PDFCRAFT_PRINT_DUPLEX"), "long-edge");
|
||||
}
|
||||
|
||||
/// Run a PowerShell script for the Windows-only tests: its stdout, or everything it said when
|
||||
/// it failed — the tests print that as the skip reason.
|
||||
#[cfg(windows)]
|
||||
fn windows_powershell(script: &str, envs: &[(&str, &str)]) -> Result<String, String> {
|
||||
let mut c = std::process::Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", script]);
|
||||
for (k, v) in envs {
|
||||
c.env(k, v);
|
||||
}
|
||||
match c.output() {
|
||||
Ok(o) if o.status.success() => Ok(String::from_utf8_lossy(&o.stdout).into_owned()),
|
||||
Ok(o) => Err(format!("{} {}", String::from_utf8_lossy(&o.stderr).trim(), String::from_utf8_lossy(&o.stdout).trim())),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows: the printer list comes from the spooler. A bare machine may have no printer; the
|
||||
/// list that exists has real names and at most one default.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_lists_the_printers_the_system_knows() {
|
||||
let printers = spool::printers();
|
||||
assert!(printers.iter().all(|p| !p.name.trim().is_empty()), "{printers:?}");
|
||||
assert!(printers.iter().filter(|p| p.default).count() <= 1, "{printers:?}");
|
||||
}
|
||||
|
||||
/// Windows: the embedded print script is valid PowerShell, checked on every machine — including
|
||||
/// runners that have no printer to print to.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn the_windows_print_script_parses() {
|
||||
let path = std::env::temp_dir().join(format!("pdfcraft-print-script-{}.ps1", std::process::id()));
|
||||
std::fs::write(&path, spool::WINDOWS_PRINT_SCRIPT).unwrap();
|
||||
let errors = windows_powershell(
|
||||
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile($env:PDFCRAFT_TEST_SCRIPT, [ref]$tokens, [ref]$errors) > $null; $errors.Count",
|
||||
&[("PDFCRAFT_TEST_SCRIPT", path.to_str().unwrap())],
|
||||
);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(errors.as_deref().map(str::trim), Ok("0"), "parse errors: {errors:?}");
|
||||
}
|
||||
|
||||
/// Windows, end to end: the script renders the job's sheets with Windows.Data.Pdf on any
|
||||
/// machine — CI included, no printer needed (the dry run writes the PNGs to the temporary
|
||||
/// folder) — and, where a paused test printer can be added, a real job lands in its queue.
|
||||
/// Its driver is the inbox "Generic / Text Only"; a machine that cannot add one (no rights, no
|
||||
/// driver) skips that half, as the CUPS tests skip where CUPS isn't running.
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn the_print_script_renders_the_job_and_reaches_the_spooler() {
|
||||
let pdf = impose(&fixture(2), &settings(vec![0, 1], Layout::Size(SizeMode::Fit))).unwrap();
|
||||
// The printer name is deliberately wrong: the dry run stops before the driver is touched.
|
||||
let job = Job { printer: Some("no such printer".into()), title: "test.pdf".into(), ..Job::default() };
|
||||
let dry = spool::windows_submit(&pdf, &job, &[("PDFCRAFT_PRINT_DRYRUN", "1")]);
|
||||
assert_eq!(dry.as_deref(), Ok("rendered 2 sheet(s)"), "{dry:?}");
|
||||
|
||||
const NAME: &str = "PdfCraft Test Print";
|
||||
let added = windows_powershell(
|
||||
&format!(
|
||||
"if (-not (Get-Printer -Name '{NAME}' -ErrorAction SilentlyContinue)) {{ Add-Printer -Name '{NAME}' -DriverName 'Generic / Text Only' -PortName 'FILE:' }}; Set-Printer -Name '{NAME}' -IsPaused $true"
|
||||
),
|
||||
&[],
|
||||
);
|
||||
if let Err(why) = added {
|
||||
eprintln!("skipping the real print: the test printer could not be prepared: {why}");
|
||||
return;
|
||||
}
|
||||
let job = Job { printer: Some(NAME.into()), title: "test.pdf".into(), ..Job::default() };
|
||||
let sent = spool::submit(&pdf, &job);
|
||||
let queued = windows_powershell(
|
||||
"if (Get-PrintJob -Name 'PdfCraft Test Print') { (Get-PrintJob -Name 'PdfCraft Test Print' | Measure-Object).Count } else { 0 }",
|
||||
&[],
|
||||
);
|
||||
let cleaned = windows_powershell("Get-PrintJob -Name 'PdfCraft Test Print' | Remove-PrintJob; Remove-Printer -Name 'PdfCraft Test Print'", &[]);
|
||||
assert!(sent.is_ok(), "{sent:?}");
|
||||
assert_eq!(queued.as_deref().map(str::trim), Ok("1"), "the job is in the paused printer's queue: {queued:?}");
|
||||
assert!(cleaned.is_ok(), "the test printer is removed again: {cleaned:?}");
|
||||
}
|
||||
|
||||
/// A PPD shaped like a Fiery's: installable options, multi-line PostScript in the choices, Latin-1
|
||||
/// labels, options the Print dialog sets itself.
|
||||
const FIERY_PPD: &[u8] = b"*PPD-Adobe: \"4.3\"\n\
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Print a print-ready PDF on Windows (issue #756). The job arrives in PDFCRAFT_* environment
|
||||
# variables, never as arguments: printer names contain spaces and quotes, and the document's
|
||||
# path must not be visible in the process list while it prints.
|
||||
#
|
||||
# The in-box Windows.Data.Pdf renders each sheet at the printer's own resolution and
|
||||
# System.Drawing.Printing spools it with the driver's settings — copies, collation, duplex and
|
||||
# colour reach the spooler as the job's, the way `lp -o` sends them on CUPS. No PDF reader is
|
||||
# needed, and the calling code touches no Win32 API.
|
||||
#
|
||||
# PDFCRAFT_PRINT_DRYRUN=1 renders every sheet to a PNG in a temporary folder and prints
|
||||
# nothing: the tests run the whole rendering path on machines without a printer, CI included.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||
try {
|
||||
$null = [Windows.Data.Pdf.PdfDocument, Windows.Data.Pdf, ContentType = WindowsRuntime]
|
||||
} catch {
|
||||
throw 'this Windows installation cannot render PDFs: the Windows.Data.Pdf component is missing'
|
||||
}
|
||||
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
|
||||
|
||||
# Await the WinRT asynchronous APIs from .NET Framework's PowerShell. AsTask is an extension
|
||||
# method: found by reflecting over the static class — with `.`, an instance call on the type
|
||||
# object, since `GetMethods` is not static and `::` would not find it.
|
||||
$type = [System.WindowsRuntimeSystemExtensions]
|
||||
$opTask = $type.GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1'
|
||||
} | Select-Object -First 1
|
||||
$actTask = $type.GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncAction'
|
||||
} | Select-Object -First 1
|
||||
function AwaitOp($operation, $resultType) {
|
||||
$task = $opTask.MakeGenericMethod($resultType).Invoke($null, @($operation))
|
||||
$task.Wait()
|
||||
$task.Result
|
||||
}
|
||||
function AwaitAct($action) {
|
||||
$task = $actTask.Invoke($null, @($action))
|
||||
$task.Wait()
|
||||
}
|
||||
|
||||
# One page rendered the size it will be drawn (`$drawW` × `$drawH`, hundredths of an inch), at
|
||||
# `$dpi` dots per inch, aspect-fitted. The caller disposes the bitmap.
|
||||
function Render-Sheet($page, $drawW, $drawH, $dpi) {
|
||||
$w = $page.Size.Width
|
||||
$h = $page.Size.Height
|
||||
$scale = [Math]::Min($drawW / 100.0 * 96.0 / $w, $drawH / 100.0 * 96.0 / $h)
|
||||
$options = [Windows.Data.Pdf.PdfPageRenderOptions]::new()
|
||||
$options.DestinationWidth = [int][Math]::Ceiling($w * $scale * $dpi / 96.0)
|
||||
$options.DestinationHeight = [int][Math]::Ceiling($h * $scale * $dpi / 96.0)
|
||||
$stream = [System.IO.MemoryStream]::new()
|
||||
try {
|
||||
$random = [System.IO.WindowsRuntimeStreamExtensions]::AsRandomAccessStream($stream)
|
||||
AwaitAct ($page.RenderToStreamAsync($random, $options))
|
||||
$stream.Position = 0
|
||||
# Copy out of the stream: GDI+ reads an Image lazily and needs its stream alive, so the
|
||||
# render below releases the stream with the function.
|
||||
$png = [System.Drawing.Image]::FromStream($stream)
|
||||
try { return [System.Drawing.Bitmap]::new($png) } finally { $png.Dispose() }
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$source = AwaitOp ([Windows.Storage.StorageFile]::GetFileFromPathAsync($env:PDFCRAFT_PRINT_FILE)) ([Windows.Storage.StorageFile])
|
||||
$document = AwaitOp ([Windows.Data.Pdf.PdfDocument]::LoadFromFileAsync($source)) ([Windows.Data.Pdf.PdfDocument])
|
||||
if ($document.PageCount -lt 1) { throw 'the print-ready PDF has no pages' }
|
||||
|
||||
if ($env:PDFCRAFT_PRINT_DRYRUN -eq '1') {
|
||||
# Every sheet rendered as the printer would take it, to prove the path without one.
|
||||
$folder = Join-Path ([System.IO.Path]::GetTempPath()) ('pdfcraft-dryrun-' + $PID)
|
||||
$null = New-Item -ItemType Directory -Force $folder
|
||||
for ($i = 0; $i -lt $document.PageCount; $i++) {
|
||||
$page = $document.GetPage($i)
|
||||
try {
|
||||
$bitmap = Render-Sheet $page 750 1000 150
|
||||
try { $bitmap.Save((Join-Path $folder "sheet-$i.png"), [System.Drawing.Imaging.ImageFormat]::Png) } finally { $bitmap.Dispose() }
|
||||
} finally {
|
||||
$page.Dispose()
|
||||
}
|
||||
}
|
||||
Write-Output ('rendered ' + $document.PageCount + ' sheet(s)')
|
||||
return
|
||||
}
|
||||
|
||||
$printer = [System.Drawing.Printing.PrintDocument]::new()
|
||||
$printer.DocumentName = $env:PDFCRAFT_PRINT_TITLE
|
||||
$settings = $printer.PrinterSettings
|
||||
if ($env:PDFCRAFT_PRINT_PRINTER) {
|
||||
try {
|
||||
$settings.PrinterName = $env:PDFCRAFT_PRINT_PRINTER
|
||||
} catch {
|
||||
throw ('the printer is not available: ' + $env:PDFCRAFT_PRINT_PRINTER)
|
||||
}
|
||||
}
|
||||
$settings.Copies = [Math]::Min(32767, [Math]::Max(1, [int]$env:PDFCRAFT_PRINT_COPIES))
|
||||
$settings.Collate = ($env:PDFCRAFT_PRINT_COLLATE -eq '1')
|
||||
$settings.Duplex = switch ($env:PDFCRAFT_PRINT_DUPLEX) {
|
||||
'long-edge' { [System.Drawing.Printing.Duplex]::Horizontal }
|
||||
'short-edge' { [System.Drawing.Printing.Duplex]::Vertical }
|
||||
default { [System.Drawing.Printing.Duplex]::Simplex }
|
||||
}
|
||||
if ($env:PDFCRAFT_PRINT_GRAYSCALE -eq '1') { $printer.DefaultPageSettings.Color = $false }
|
||||
|
||||
# One sheet at a time: its page is rendered when the driver asks for it, so a long document
|
||||
# holds only one page's bitmap in memory.
|
||||
$script:sheets = 0
|
||||
$printer.add_PrintPage({
|
||||
param($sender, $e)
|
||||
if ($script:sheets -ge $document.PageCount) { $e.HasMorePages = $false; return }
|
||||
$page = $document.GetPage($script:sheets)
|
||||
$bitmap = $null
|
||||
try {
|
||||
# The driver's page area is in hundredths of an inch; Render-Sheet fits the page into
|
||||
# it, so this is the same scale, turned back into hundredths for the draw rectangle.
|
||||
$area = $e.MarginBounds
|
||||
$w = $page.Size.Width
|
||||
$h = $page.Size.Height
|
||||
$scale = [Math]::Min($area.Width / 100.0 * 96.0 / $w, $area.Height / 100.0 * 96.0 / $h)
|
||||
$dpi = [Math]::Min(600, [Math]::Max(72, $e.Graphics.DpiX))
|
||||
$bitmap = Render-Sheet $page $area.Width $area.Height $dpi
|
||||
$drawW = $w * $scale / 96.0 * 100.0
|
||||
$drawH = $h * $scale / 96.0 * 100.0
|
||||
$box = [System.Drawing.RectangleF]::new($area.Left + ($area.Width - $drawW) / 2.0, $area.Top + ($area.Height - $drawH) / 2.0, $drawW, $drawH)
|
||||
$e.Graphics.DrawImage($bitmap, $box)
|
||||
} finally {
|
||||
if ($bitmap) { $bitmap.Dispose() }
|
||||
$page.Dispose()
|
||||
}
|
||||
$script:sheets++
|
||||
$e.HasMorePages = ($script:sheets -lt $document.PageCount)
|
||||
})
|
||||
$printer.Print()
|
||||
Write-Output ('spooled ' + $document.PageCount + ' sheet(s)')
|
||||
@@ -108,12 +108,12 @@ fn view_history_select_all_and_find_options() {
|
||||
h.state_mut().views[0].open_find();
|
||||
h.state_mut().views[0].find.as_mut().unwrap().query = "page".into();
|
||||
h.state_mut().views[0].rerun_find();
|
||||
for _ in 0..60 {
|
||||
// Find runs in the background: wait for it by a deadline, not a frame count, which a loaded
|
||||
// machine (the whole workspace testing at once) can use up before the last page is searched.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
while std::time::Instant::now() < deadline && h.state().views[0].find.as_ref().unwrap().matches.len() < 15 {
|
||||
h.run_steps(2);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
if h.state().views[0].find.as_ref().unwrap().matches.len() >= 15 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(h.state().views[0].find.as_ref().unwrap().matches.len(), 15);
|
||||
{
|
||||
|
||||
+8
-6
@@ -22,7 +22,7 @@ update the numbers.
|
||||
| 1 | Encrypted files we write don't open in Acrobat | file format | 8–15 | [file-format-parity.md](file-format-parity.md) |
|
||||
| 2 | Rendering is borrowed and its fidelity unmeasured | feature, spec | 110–190 | [pdf-spec-parity.md](pdf-spec-parity.md) |
|
||||
| 3 | Editing existing text is fragile on real files | feature | 100–180 | [target-app-parity.md](target-app-parity.md) |
|
||||
| 4 | No printing on Windows or the web | feature, platform | 15–30 | [hardware-parity.md](hardware-parity.md) |
|
||||
| 4 | No printing on the web | feature, platform | 15–30 | [hardware-parity.md](hardware-parity.md) |
|
||||
| 5 | Chinese UI shows missing-glyph boxes in releases | localization | 6–12 | [localization-parity.md](localization-parity.md) |
|
||||
| 6 | Open-issue backlog of wrong results in shipped features | stability | 45–80 | this file |
|
||||
| 7 | OCR reads only unaccented Latin | feature | 40–75 | [file-format-parity.md](file-format-parity.md) |
|
||||
@@ -97,12 +97,14 @@ update the numbers.
|
||||
how a document looks.
|
||||
- **Estimate:** 100–180 h (the longest feature pole, M7). **Doc:** [target-app-parity.md](target-app-parity.md).
|
||||
|
||||
### 4. No printing on Windows or the web
|
||||
### 4. No printing on the web
|
||||
|
||||
- **Missing:** a Windows spooler (and printer properties), browser printing.
|
||||
- **Evidence:** [#756](https://github.com/storytold/pdfcraft/issues/756): on Windows 11 the Print
|
||||
dialog only saves a PDF. `crates/print/README.md`: "Not yet: Windows and web spoolers".
|
||||
- **Impact:** Windows is most of Acrobat's market. Printing is a P0 workflow.
|
||||
- **Missing:** browser printing.
|
||||
- **Evidence:** the web build has no spooler: off Windows and unix, `spool::submit` reports that
|
||||
printing to a printer isn't available and only the print-ready PDF can be saved.
|
||||
`crates/print/README.md`: "Not yet: the web spooler".
|
||||
- **Impact:** the web build cannot send a job to a printer; printing is a P0 workflow. Printing on
|
||||
Windows shipped with [#756](https://github.com/storytold/pdfcraft/issues/756).
|
||||
- **Estimate:** 15–30 h. **Doc:** [hardware-parity.md](hardware-parity.md).
|
||||
|
||||
### 5. Chinese UI shows missing-glyph boxes in releases
|
||||
|
||||
@@ -31,7 +31,7 @@ printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30
|
||||
| Platform | Acrobat | PdfCraft | Packaging | Gaps |
|
||||
|---|---|---|---|---|
|
||||
| macOS | yes | yes | signed and notarized DMG | — |
|
||||
| Windows (x64, x86, ARM64) | yes (x64, ARM64) | yes | signed installers | printing (#756) |
|
||||
| Windows (x64, x86, ARM64) | yes (x64, ARM64) | yes | signed installers | — |
|
||||
| Linux | no | yes | AppImage, deb, rpm, Flatpak, tarball | Chinese faces in Flatpak (#689) |
|
||||
| FreeBSD | no | yes | package | — |
|
||||
| Web | Acrobat online (cloud) | yes (WASM, offline in the browser) | static site | printing, OCR, crash recovery, PKCS #12 signing, hosted deployment |
|
||||
@@ -41,3 +41,4 @@ printing are counted in their feature areas). **Platforms ≈ 70% ready, 15–30
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | major | Created from the code, `vendor/README.md`, `.github/workflows/release.yml`, the print crate's README and user issues |
|
||||
| 2026-10-10 | minor | Windows printing shipped (#756): the Windows row's gap is closed; the web row keeps "printing" |
|
||||
+3
-3
@@ -21,7 +21,7 @@ exchange with Acrobat users or a missing sub-case is `partial, not blocking` (a
|
||||
|
||||
| Core workflow | Works end to end? | Evidence | Hours to pass |
|
||||
|---|---|---|---|
|
||||
| View, search and print | yes | Render, find, select, panels, tiles; 500-page scrolling test; Print with CUPS on macOS and Linux (Windows printing missing, #756, a beta item). Rendering borrowed from `hayro` | 0 |
|
||||
| View, search and print | yes | Render, find, select, panels, tiles; 500-page scrolling test; Print with CUPS on macOS/Linux and the Windows spooler (web printing missing, a beta item). Rendering borrowed from `hayro` | 0 |
|
||||
| Organize, combine and split pages | yes | `organize.*` P0s shipped with tests; links, fields, layers and bookmarks carried over; open fixes #817, #821 are edge cases | 0 |
|
||||
| Comment and review | yes | Every markup type with appearances, replies, status, XFDF/FDF; comments saved in the PDF reopen in other readers. XFDF fidelity bugs #809, #819 | 0 |
|
||||
| Fill and sign forms | yes | All AcroForm field types, Acrobat's AF functions, Fill & Sign, PAdES signatures validated with `pdfsig`/OpenSSL | 0 |
|
||||
@@ -42,7 +42,7 @@ In order. Each item names its entry in [gaps.md](gaps.md).
|
||||
damaging), then Create PDF from Office and a Word export that keeps layout (gap 8, #773).
|
||||
2. **Fix the crash and wrong-result reports** filed against shipped features (gap 6), starting with
|
||||
the panic in #816.
|
||||
3. **Printing on Windows** (gap 4, #756) and **a Chinese UI face in releases** (gap 5, #826).
|
||||
3. **A Chinese UI face in releases** (gap 5, #826).
|
||||
4. **A fidelity harness against Acrobat, then the renderer decision** (gaps 12 and 2). Owner
|
||||
decision needed: our own M2 devices, or #841's proposal as the bridge.
|
||||
5. **Editing existing content** (gap 3, M7): fonts, subsets, reflow, CJK and RTL.
|
||||
@@ -74,7 +74,7 @@ total matches the parity estimate.
|
||||
| M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks |
|
||||
| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.<br>Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding |
|
||||
| M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.<br>Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, CUPS spooler, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; Create PDF from multiple files (PDFs, images and text, edited as pages in the grid). Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, Windows/web printing |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; Create PDF from multiple files (PDFs, images and text, edited as pages in the grid). Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, web printing |
|
||||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
|
||||
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools).<br>Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers |
|
||||
|
||||
@@ -207,7 +207,7 @@ full parity in that area.
|
||||
| I Scan and OCR | 26 | 5 | 2 | 23.1% | 15% | 40–75 | Searchable image (ocrs, Latin script without accents) on pages, ranges and many files. Missing: every other language (#744 Dutch; accents, CJK, Cyrillic, Arabic), editable-text output, deskew, rotation, despeckle, camera images, MRC compression, scanners |
|
||||
| J Create and export | 34 | 12 | 3 | 39.7% | 25% | 55–100 | Create from images, multipage TIFF, text, clipboard, blank, many files at once; export PNG/JPEG/TIFF, text, RTF; Word and HTML partial. Missing: Office/HTML/web/PostScript to PDF, Excel and PowerPoint export, Word layout fidelity (#773), SVG/XML/EPS/PS/JPEG 2000 export, accessible text |
|
||||
| K Optimize and standards | 58 | 15 | 10 | 34.5% | 20% | 65–115 | Reduce File Size and the Optimizer (images, discards, clean-up, space audit); PDF/A-2b/3b verify and convert (partial). Missing: Preflight (profiles, fixups, reports), PDF/A-1/4 and a/u levels, PDF/X, PDF/UA, PDF/E, PDF/VT, transparency flattening, font unembedding, monochrome JBIG2/CCITT |
|
||||
| L Print | 39 | 15 | 1 | 39.7% | 35% | 30–55 | Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, print-ready PDF, CUPS on macOS/Linux. Missing: **printing on Windows (#756) and the web**, print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) |
|
||||
| L Print | 39 | 15 | 1 | 39.7% | 35% | 30–55 | Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, print-ready PDF, CUPS on macOS/Linux and the Windows spooler (#756). Missing: **printing on the web**, print as image, PostScript, all print-production tools (output preview, separations, ink manager, printer marks, colour conversion) |
|
||||
| M Accessibility | 61 | 38 | 1 | 63.1% | 30% | 40–70 | Accessibility Checker (all 32 rules, report, fixes), alternate text, title and language. Missing: autotag, Tags/Order/Content panels, Reading Order tool, artifact marking, table editor, Read Out Loud, full keyboard operation of the app, screen-reader audit |
|
||||
| N Automation, preferences, misc | 75 | 20 | 5 | 30.0% | 35% | 25–45 | CLI, MCP, UI control channel, 141 automation tools, Action Wizard (run, create), compare (text, visual, report). Missing: most Preferences pages, side-by-side compare, Action Wizard management, custom commands, AI providers, rich media/3D, hosted web app |
|
||||
| **All** | **808** | **423** | **74** | **56.9%** | **≈ 49%** | **610–1,100** | Plus 175–325 h of cross-cutting work (dimension table) |
|
||||
|
||||
@@ -7866,8 +7866,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["printers", "printer_options", "doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options"]
|
||||
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; Save as PDF; Windows spooler pending. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job"
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::driverless_printers_join_the_list_without_a_queue", "crates/print/src/tests.rs::windows_printers_carry_one_default_and_keep_their_names", "crates/print/src/tests.rs::windows_lists_the_printers_the_system_knows", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler", "crates/print/src/tests.rs::printer_options_come_from_the_ppd", "crates/print/src/tests.rs::printer_option_defaults_and_job_arguments", "crates/ui-egui/tests/print.rs::print_dialog_shows_the_printer_driver_options"]
|
||||
notes = "CUPS queues via lpstat -p -d and driverless destinations without a queue via lpstat -e; on Windows the queues and the default come from Win32_Printer (PowerShell CIM) and jobs are rendered with Windows.Data.Pdf and spooled with the driver's own settings; Save as PDF on every platform. Properties…: on CUPS the driver's own PPD job options (tray, paper type, finishing …, not installable options or what the dialog sets) for this job, sent as lp -o; on Windows the driver's Printing Preferences window (printui), saved as that printer's per-user defaults rather than per job"
|
||||
|
||||
[[feature]]
|
||||
id = "print.copies-collate"
|
||||
@@ -7878,8 +7878,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list"]
|
||||
notes = "lp -n, collate option"
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler"]
|
||||
notes = "lp -n, collate option; on Windows the driver's Copies and Collate settings, as the job's"
|
||||
|
||||
[[feature]]
|
||||
id = "print.page-range"
|
||||
@@ -7938,8 +7938,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list"]
|
||||
notes = "long/short edge via the spooler"
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script", "crates/print/src/tests.rs::a_print_job_reaches_the_windows_spooler"]
|
||||
notes = "long/short edge via the spooler: lp -o sides=…, and the Windows driver's Duplex setting"
|
||||
|
||||
[[feature]]
|
||||
id = "print.n-up"
|
||||
@@ -8034,8 +8034,8 @@ milestone = "M10"
|
||||
status = "partial"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list"]
|
||||
notes = "asks the printer for monochrome; the PDF itself stays in colour"
|
||||
evidence = ["crates/print/src/tests.rs::spooler_arguments_and_printer_list", "crates/print/src/tests.rs::the_windows_job_is_the_environment_of_the_print_script"]
|
||||
notes = "asks the printer for monochrome (lp -o, and the Windows page settings as the job's); the PDF itself stays in colour"
|
||||
|
||||
[[feature]]
|
||||
id = "print.print-as-image"
|
||||
|
||||
Reference in new issue
Block a user