From a07cdf87f0eb40e49e0f64ddfafbca6f33fade3c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 21:29:50 -0800 Subject: [PATCH] PDF/X: names inside a string or a comment no longer fail the standard check The check that a PDF/X-1a or PDF/X-3 file has no transparency read names in literal strings and comments as syntax. The file identifier is a base64 hash of the file, so a file could now and then hold `/BM` or `/Lab` there and its export was refused as having blend modes or Lab colors. The check now leaves out the text of literal strings and comments as well as stream data. --- crates/pdf/src/pdfx.rs | 47 +++++++++++++++++++++++++++--------- crates/pdf/src/tests/pdfx.rs | 20 +++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/crates/pdf/src/pdfx.rs b/crates/pdf/src/pdfx.rs index 116d1cf0c..e367221d5 100644 --- a/crates/pdf/src/pdfx.rs +++ b/crates/pdf/src/pdfx.rs @@ -211,23 +211,46 @@ pub(crate) fn verify(pdf: &[u8], standard: Standard) -> Result<(), PdfError> { Ok(()) } -/// `pdf` without its streams' data (dictionaries and the rest of the syntax), so that what is -/// looked for there isn't found in image or font bytes. +/// `pdf` without its streams' data, the text of its literal strings and its comments (dictionaries +/// and the rest of the syntax), so that what is looked for there isn't found in image or font +/// bytes, or in a string such as the file identifier, whose base64 hash can hold `/BM`. fn without_streams(pdf: &[u8]) -> Vec { let mut out = Vec::with_capacity(pdf.len().min(1 << 20)); let mut at = 0; - while let Some(s) = find(pdf, b"stream", at) { - let data = s + b"stream".len(); - // `endstream` itself, or a stream keyword that isn't one (inside a name or string). - if (s >= 3 && pdf.get(s - 3..s) == Some(b"end")) || !pdf.get(data).is_some_and(|b| matches!(b, b'\r' | b'\n')) { - out.extend_from_slice(pdf.get(at..data).unwrap_or_default()); - at = data; - continue; + while let Some(&b) = pdf.get(at) { + match b { + // A literal string: parentheses nest, and a backslash escapes the next byte. + b'(' => { + out.extend_from_slice(b"()"); + let mut depth = 0usize; + at += 1; + while let Some(&c) = pdf.get(at) { + at += 1; + match c { + b'\\' => at += 1, + b'(' => depth += 1, + b')' if depth == 0 => break, + b')' => depth -= 1, + _ => {} + } + } + } + // A comment, to the end of its line. + b'%' => at += pdf.get(at..).unwrap_or_default().iter().take_while(|c| !matches!(c, b'\r' | b'\n')).count(), + // A stream keyword (not `endstream`, and followed by its end of line): its data is left out. + b's' if pdf.get(at..at + 6) == Some(b"stream") + && !(at >= 3 && pdf.get(at - 3..at) == Some(b"end")) + && pdf.get(at + 6).is_some_and(|c| matches!(c, b'\r' | b'\n')) => + { + out.extend_from_slice(b"stream"); + at = find(pdf, b"endstream", at + 6).unwrap_or(pdf.len()); + } + _ => { + out.push(b); + at += 1; + } } - out.extend_from_slice(pdf.get(at..data).unwrap_or_default()); - at = find(pdf, b"endstream", data).unwrap_or(pdf.len()); } - out.extend_from_slice(pdf.get(at..).unwrap_or_default()); out } diff --git a/crates/pdf/src/tests/pdfx.rs b/crates/pdf/src/tests/pdfx.rs index dbc221e0d..ec22eb018 100644 --- a/crates/pdf/src/tests/pdfx.rs +++ b/crates/pdf/src/tests/pdfx.rs @@ -224,3 +224,23 @@ fn verify_finds_what_each_standard_forbids() { assert!(matches!(verify(&file(font), Standard::PdfX4), Err(PdfError::Unsupported(m)) if m.contains("fonts"))); assert!(verify(&file("<>"), Standard::PdfX4).is_ok()); } + +/// Names inside a string or a comment are text, not syntax: a file identifier, whose base64 hash +/// can hold `/BM` or `/Lab`, does not make a file break its standard. +#[test] +fn verify_skips_names_in_strings_and_comments() { + use crate::pdfx::verify; + let file = |body: &str| format!("%PDF-1.3\n%/BM/Multiply\n1 0 obj\n{body}\nendobj\n").into_bytes(); + for ok in [ + "<>", + "<>", + "<>", + ] { + for standard in [Standard::PdfX1a, Standard::PdfX3] { + assert!(verify(&file(ok), standard).is_ok(), "{standard:?}: {ok}"); + } + } + // A name after a string is still read. + let after = "<>"; + assert!(matches!(verify(&file(after), Standard::PdfX3), Err(PdfError::Unsupported(m)) if m.contains("blend modes"))); +}