Merge pull request #6795 from dotCooCoo/fix/pdfx-check-skips-strings

PDF/X: names inside a string or a comment no longer fail the standard check
This commit is contained in:
BFlat
2026-11-12 14:16:50 +07:00
committed by GitHub
2 changed files with 55 additions and 12 deletions
+35 -12
View File
@@ -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<u8> {
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
}
+20
View File
@@ -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("<</Type/FontDescriptor/FontFile2 4 0 R>>"), 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 [
"<</Size 2/ID[(q/BM+Zt1kR0x/Lab=) (q/BM+Zt1kR0x/Lab=)]>>",
"<</Title(a\\)/SMask 5 0 R)/Subject(a(b)/ca 0.5)>>",
"<</Title(stream\n/BM/Multiply\nendstream)>>",
] {
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 = "<</Title(a(b)c)/BM/Multiply>>";
assert!(matches!(verify(&file(after), Standard::PdfX3), Err(PdfError::Unsupported(m)) if m.contains("blend modes")));
}