fix(print): refuse fitting that would discard annotations

This commit is contained in:
skylartaylor
2026-10-13 01:47:19 +02:00
parent f03ec27ab2
commit ccb1eae613
3 changed files with 41 additions and 0 deletions
+4
View File
@@ -61,6 +61,10 @@ a driver paper-size mismatch. On Windows `command_preview` describes the in-proc
(`xps-print`, then the printer and options as `key=value`) instead of a command line. The crate
does not provide PostScript output, separations, trapping or colour-managed device rendering.
Windows shrink-to-fit refuses pages with annotations when their geometry needs fitting.
Callers must flatten annotation appearances into page content first. Exact-size printing and
rotation retain the annotations. This avoids losing printed comments, forms or other marks.
`cargo test -p craft-print` tests CUPS transport (including pipe-sized jobs and refusal),
printer parsing, PPD options and argument mapping, and on every platform the Windows pieces that
need no printer: the PrintTicket, the checks on the driver's validated ticket and capabilities,
+9
View File
@@ -1068,6 +1068,15 @@ impl Sheets<'_> {
if get(&self.trailer, b"Encrypt").is_some() {
return Err(refuse("it is encrypted"));
}
// Annotation appearances live outside /Contents. Transforming only content
// would leave them at the old coordinates; deleting them loses printed marks.
// Require callers to flatten before fitting until appearances can be reframed.
if let Some(annotations) = get(&dict, b"Annots")
&& !matches!(annotations, Obj::Null)
&& !matches!(annotations, Obj::Array(items) if items.is_empty())
{
return Err(refuse("flatten page annotations before using shrink-to-fit"));
}
let [a, b, c, d, e, f] = frame.matrix;
let [x, y, w, h] = frame.clip;
let prefix = format!(
+28
View File
@@ -49,6 +49,10 @@ const TABLOID: TestPage = TestPage { media: Some([0.0, 0.0, 792.0, 1224.0]), cro
/// cross-reference stream, as modern writers save. The root page node sets a Letter MediaBox
/// for pages that inherit it.
fn test_pdf(pages: &[TestPage], compressed: bool) -> Vec<u8> {
test_pdf_with_annotations(pages, compressed, None)
}
fn test_pdf_with_annotations(pages: &[TestPage], compressed: bool, annotations: Option<&str>) -> Vec<u8> {
let n = pages.len();
// 1 catalog, 2 page tree, 3 content, then the pages.
let kids: Vec<String> = (0..n).map(|i| format!("{} 0 R", 4 + i)).collect();
@@ -69,6 +73,9 @@ fn test_pdf(pages: &[TestPage], compressed: bool) -> Vec<u8> {
if let Some(r) = p.rotate {
page += &format!(" /Rotate {r}");
}
if let Some(annotations) = annotations {
page += &format!(" /Annots {annotations}");
}
objects.push(page + " >>");
}
let mut pdf = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n".to_vec();
@@ -264,6 +271,27 @@ fn shrink_to_fit_reframes_sheets_on_the_paper() {
assert_eq!(shapes(&sheets.update(&edits).unwrap()), [((612, 792), 0), ((612, 792), 270), ((612, 792), 0)]);
}
#[test]
fn shrink_to_fit_refuses_to_drop_annotation_appearances() {
use crate::sheets::{Fit, plan, read};
for annotations in ["[<< /Subtype /Text /Rect [36 36 72 72] /F 4 >>]", "42 0 R"] {
let pdf = test_pdf_with_annotations(&[LETTER, LETTER_LANDSCAPE], false, Some(annotations));
let sheets = read(&pdf).unwrap();
let fit = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }).unwrap();
assert!(matches!(sheets.update(&fit), Err(Error::Spool(ref m)) if m.contains("flatten page annotations")));
let exact = plan(&sheets.sheets, (612.0, 792.0), Fit::Exact).unwrap();
let rotated = sheets.update(&exact).unwrap();
assert!(rotated.starts_with(&pdf));
assert!(String::from_utf8_lossy(&rotated[pdf.len()..]).contains("/Annots"), "rotation retains annotations on the updated page");
}
for annotations in ["[]", "null"] {
let pdf = test_pdf_with_annotations(&[LETTER, LETTER_LANDSCAPE], false, Some(annotations));
let sheets = read(&pdf).unwrap();
let fit = plan(&sheets.sheets, (612.0, 792.0), Fit::Shrink { margin: 18.0 }).unwrap();
assert!(sheets.update(&fit).is_ok());
}
}
#[test]
fn an_encrypted_file_is_turned_but_not_reframed() {
// An /Encrypt entry is all it takes: new content streams would have to be encrypted too.