mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 20:56:47 -06:00
PDF: a placed PDF 1.7 with /Version /1.7 in its catalog goes into PDF/X-4 exports (fixes #907)
This commit is contained in:
@@ -850,6 +850,45 @@ mod placed_pdf_print_tests {
|
||||
let e = s.execute("file.exportPdf", &json!({"standard": "x4"})).unwrap_err().to_string();
|
||||
assert!(e.contains("logo.pdf is PDF 2.0") && e.contains("PDF 1.6"), "{e}");
|
||||
}
|
||||
|
||||
/// A one-page PDF with a black box whose catalog says `/Version /1.7`, as TCPDF writes it.
|
||||
fn catalog_v17(header: &str) -> Vec<u8> {
|
||||
let content = "0 0 0 1 k 0 0 200 200 re f";
|
||||
let mut pdf = format!("{header}\n").into_bytes();
|
||||
let mut offsets = Vec::new();
|
||||
for body in [
|
||||
"<</Type/Catalog/Pages 2 0 R/Version /1.7>>".to_owned(),
|
||||
"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_owned(),
|
||||
"<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]/Contents 4 0 R/Resources<<>>>>".to_owned(),
|
||||
format!("<</Length {}>>\nstream\n{content}\nendstream", content.len()),
|
||||
] {
|
||||
offsets.push(pdf.len());
|
||||
pdf.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", offsets.len()).as_bytes());
|
||||
}
|
||||
let xref = pdf.len();
|
||||
pdf.extend_from_slice(b"xref\n0 5\n0000000000 65535 f\r\n");
|
||||
for offset in offsets {
|
||||
pdf.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
|
||||
}
|
||||
pdf.extend_from_slice(format!("trailer\n<</Size 5/Root 1 0 R>>\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
||||
pdf
|
||||
}
|
||||
|
||||
/// A placed PDF whose catalog `/Version /1.7` overrides its header failed the PDF/X-4 export
|
||||
/// with "logo.pdf is PDF 1.7", though a 1.7 header alone is relabelled (#907).
|
||||
#[test]
|
||||
fn catalog_version_17_placed_in_pdfx4() {
|
||||
for header in ["%PDF-1.7", "%PDF-1.4"] {
|
||||
let pdf = catalog_v17(header);
|
||||
assert_eq!(hayro_syntax::Pdf::new(pdf.clone()).unwrap().version(), hayro_syntax::PdfVersion::Pdf17);
|
||||
let mut s = page_with(&pdf);
|
||||
let r = s.execute("file.exportPdf", &json!({"standard": "x4"})).unwrap_or_else(|e| panic!("{header}: {e}"));
|
||||
assert!(r["warnings"].to_string().contains("logo.pdf: a PDF 1.7 placed in a PDF 1.6 file"), "{}", r["warnings"]);
|
||||
let out = base64_decode(r["base64"].as_str().unwrap());
|
||||
assert!(out.starts_with(b"%PDF-1.6"));
|
||||
assert_eq!(hayro_syntax::Pdf::new(out).unwrap().version(), hayro_syntax::PdfVersion::Pdf16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1172,7 +1172,8 @@ impl Exporter<'_> {
|
||||
/// A placed PDF as krilla embeds it. krilla refuses a PDF newer than the file it writes (the
|
||||
/// export fails at the end with `VersionMismatch(Pdf17)`), and PDF/X-4 is PDF 1.6 while
|
||||
/// VectorCraft and most tools write 1.7. PDF 1.7 changed nothing in how a page draws, so a 1.7
|
||||
/// file goes in as 1.6 (its header relabelled, as `qpdf --force-version=1.6` does). A newer
|
||||
/// file goes in as 1.6 (its header and catalog `/Version` relabelled, as
|
||||
/// `qpdf --force-version=1.6` does). A newer
|
||||
/// file is left out and the export fails naming it ([`Exporter::check_placed_versions`]).
|
||||
fn placed_pdf(&mut self, asset: &designcraft_doc::Asset) -> Option<krilla::pdf::PdfDocument> {
|
||||
use hayro_syntax::PdfVersion as V;
|
||||
@@ -1193,8 +1194,8 @@ impl Exporter<'_> {
|
||||
}
|
||||
if have == V::Pdf17
|
||||
&& max == V::Pdf16
|
||||
&& let Some(bytes) = relabel_pdf_header(&asset.data, b"%PDF-1.7", b"%PDF-1.6")
|
||||
&& let Ok(pdf) = krilla::pdf::Pdf::new(Arc::new(bytes))
|
||||
&& let bytes = relabel_pdf_header(&asset.data, b"%PDF-1.7", b"%PDF-1.6").unwrap_or_else(|| asset.data.to_vec())
|
||||
&& let Ok(pdf) = krilla::pdf::Pdf::new(Arc::new(relabel_catalog_version(bytes)))
|
||||
&& pdf.version() <= max
|
||||
{
|
||||
self.warn(format!("{}: a PDF 1.7 placed in a PDF 1.6 file, embedded as PDF 1.6", asset.name));
|
||||
@@ -1360,6 +1361,30 @@ fn relabel_pdf_header(data: &[u8], from: &[u8], to: &[u8]) -> Option<Vec<u8>> {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `data` with every `/Version /1.7` changed to `/Version /1.6` in place, so the xref offsets
|
||||
/// hold. The catalog's `/Version` wins over the header (TCPDF writes both), so relabelling the
|
||||
/// header alone leaves the file a PDF 1.7. A catalog inside a compressed object stream is left as
|
||||
/// it is, and the version check still names the file.
|
||||
fn relabel_catalog_version(mut data: Vec<u8>) -> Vec<u8> {
|
||||
const KEY: &[u8] = b"/Version";
|
||||
let mut from = 0;
|
||||
while let Some(at) = data.get(from..).and_then(|rest| rest.windows(KEY.len()).position(|w| w == KEY)) {
|
||||
let mut i = from + at + KEY.len();
|
||||
while data.get(i).is_some_and(|b| b" \t\r\n\x0c\0".contains(b)) {
|
||||
i += 1;
|
||||
}
|
||||
let ends = data.get(i + 4).is_none_or(|b| !b.is_ascii_alphanumeric() && *b != b'.');
|
||||
if data.get(i..i + 4) == Some(b"/1.7".as_slice())
|
||||
&& ends
|
||||
&& let Some(b) = data.get_mut(i + 3)
|
||||
{
|
||||
*b = b'6';
|
||||
}
|
||||
from = i;
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// The sRGB profile (ICC v2.1) that untagged RGB images carry in PDF/X-4.
|
||||
pub(crate) static SRGB_ICC: &[u8] = include_bytes!("../../../assets/icc/sRGB-v2-magic.icc");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user