PDF/X-4: warn when a transparency group still blends in RGB

The CMYK rewrite matches krilla's exact output; if that ever changes,
the export says so instead of silently writing RGB groups.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-06 09:05:03 -04:00
1 parent 0ce21b476c
commit e7ad37517e
4 files changed
+16 -3

No files matched your search

+3 -1
View File
@@ -888,12 +888,14 @@ mod print_group_tests {
let t = text(&mut s);
assert!(t.contains("/S/Transparency/I true/CS/DeviceCMYK>>"), "CMYK group");
assert!(!t.contains("DeviceRGB"), "no RGB left");
assert!(!designcraft_pdf::has_rgb_groups(t.as_bytes()));
// The file still reads, with the same page.
let r = s.execute("file.exportPdf", &json!({})).unwrap();
let bytes = base64_decode(r["base64"].as_str().unwrap());
assert_eq!(hayro_syntax::Pdf::new(bytes).unwrap().pages().len(), 1);
// An RGB blend space keeps RGB groups.
s.execute("edit.transparencyBlendSpace", &json!({"space": "rgb"})).unwrap();
assert!(text(&mut s).contains("/CS/DeviceRGB"));
let t = text(&mut s);
assert!(t.contains("/CS/DeviceRGB") && designcraft_pdf::has_rgb_groups(t.as_bytes()));
}
}
+5 -1
View File
@@ -205,10 +205,14 @@ pub fn export_pdf_with_report(doc: &Document, cache: &Cache, opts: &PdfOptions)
ex.check_placed_versions()?;
let mut bytes = pdf.finish().map_err(|e| PdfError::Write(format!("{e:?}")))?;
// Transparency blends in the document's blend space: CMYK for print (and always in PDF/X).
let mut warnings = ex.warnings;
if !ex.rgb_only && (doc.settings.blend_space == designcraft_doc::BlendSpace::Cmyk || opts.standard == Standard::PdfX4) {
crate::pdfx::cmyk_group_spaces(&mut bytes);
// The rewrite matches krilla's exact output; say so if a group got past it.
if opts.standard == Standard::PdfX4 && crate::pdfx::has_rgb_groups(&bytes) {
warnings.push("PDF/X-4: some transparency groups still blend in RGB".into());
}
}
let mut warnings = ex.warnings;
// Form fields (Buttons and Forms) and, for interactive PDF, video and sound; not in PDF/X.
let fields = crate::forms::collect(doc, &sheets);
let media = if opts.media { crate::forms::collect_media(doc, &sheets) } else { Vec::new() };
+1 -1
View File
@@ -35,7 +35,7 @@ mod forms;
mod pdfx;
mod transitions;
pub use export::{BookletKind, BookletOptions, booklet_pairs, export_booklet, export_pdf, export_pdf_with_report, merge_pdfs, sheet_spreads};
pub use pdfx::{check_pdfx4, cmyk_group_spaces, make_pdfx4};
pub use pdfx::{check_pdfx4, cmyk_group_spaces, has_rgb_groups, make_pdfx4};
pub use transitions::{add_blend_space, add_catalog_entries, add_page_entries, add_transitions};
/// PDF standard to target.
+7
View File
@@ -125,6 +125,13 @@ pub fn make_pdfx4(pdf: &[u8], title: &str) -> Option<Vec<u8>> {
/// DeviceCMYK in place and at the same length (the optional `/Type/Group` gives the room), so
/// every cross-reference offset stays valid. Only object dictionaries are touched, never stream
/// data. Returns how many groups changed.
/// Whether a transparency group dictionary still names DeviceRGB as its blending space.
pub fn has_rgb_groups(pdf: &[u8]) -> bool {
pdf.windows(b"/CS/DeviceRGB".len()).enumerate().any(|(i, w)| {
w == b"/CS/DeviceRGB" && pdf.get(i.saturating_sub(64)..i).is_some_and(|before| before.windows(15).any(|b| b == b"/S/Transparency"))
})
}
pub fn cmyk_group_spaces(pdf: &mut [u8]) -> usize {
const FORMS: [(&[u8], &[u8]); 2] = [
(b"/Group<</Type/Group/S/Transparency/I true/CS/DeviceRGB>>", b"/Group<</S/Transparency/I true/CS/DeviceCMYK>>"),