#![cfg(feature = "cpu-render")]
use zpdf::{ContentInterpreter, ImageCache, PdfDocument, RenderBackend};
const SCALE: f32 = 1.0;
fn assemble(objs: &[Vec<u8>]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n".to_vec();
let mut offsets = Vec::with_capacity(objs.len());
for (i, body) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(body);
out.extend_from_slice(b"\nendobj\n");
}
let xref_pos = out.len();
let n = objs.len() + 1;
out.extend_from_slice(format!("xref\n0 {n}\n").as_bytes());
out.extend_from_slice(b"0000000000 65535 f \n");
for off in &offsets {
out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(
format!("trailer\n<< /Size {n} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n").as_bytes(),
);
out
}
fn stream_obj(dict: &str, content: &[u8]) -> Vec<u8> {
let mut v = format!("<< {dict} /Length {} >>\nstream\n", content.len()).into_bytes();
v.extend_from_slice(content);
v.extend_from_slice(b"\nendstream");
v
}
fn build(content: &[u8], resources: &str, extra: &[Vec<u8>]) -> Vec<u8> {
let mut objs = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R \
/Resources << {resources} >> >>"
)
.into_bytes(),
stream_obj("", content),
];
objs.extend_from_slice(extra);
assemble(&objs)
}
fn render(pdf: Vec<u8>) -> zpdf::cpu::RenderedPage {
let doc = PdfDocument::open(pdf).expect("open pdf");
let page = doc.page(0).expect("page 0");
let mut fonts = doc.load_page_fonts(&page);
let content = doc.page_content_bytes(&page).expect("content bytes");
let mut images = ImageCache::new();
let dl = ContentInterpreter::new(page.media_box)
.with_fonts(&mut fonts)
.with_document(doc.file(), &page.resources)
.with_images(&mut images)
.interpret(&content);
zpdf::cpu::CpuRenderer::new()
.with_fonts(&fonts)
.with_images(&images)
.render_display_list(&dl, SCALE)
.expect("cpu render")
}
fn px(page: &zpdf::cpu::RenderedPage, x: f64, y: f64) -> [u8; 3] {
let ix = (x * SCALE as f64) as u32;
let iy = ((200.0 - y) * SCALE as f64) as u32;
let off = ((iy * page.width + ix) * 4) as usize;
[page.data[off], page.data[off + 1], page.data[off + 2]]
}
fn assert_near(c: [u8; 3], want: [u8; 3], tol: u8, what: &str) {
let ok = c
.iter()
.zip(want.iter())
.all(|(a, b)| (*a as i32 - *b as i32).abs() <= tol as i32);
assert!(ok, "{what}: got {c:?}, want ≈{want:?} (±{tol})");
}
#[test]
fn none_separation_fill_paints_nothing() {
let resources = "/ColorSpace << /NoneCS [/Separation /None /DeviceCMYK \
<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>] >>";
let content = b"1 0 0 rg 0 0 200 200 re f\n\
/NoneCS cs 1 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[]));
assert_near(
px(&page, 100.0, 100.0),
[255, 0, 0],
4,
"None fill is invisible",
);
}
#[test]
fn none_separation_stroke_paints_nothing() {
let resources = "/ColorSpace << /NoneCS [/Separation /None /DeviceGray \
<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>";
let content = b"1 0 0 rg 0 0 200 200 re f\n\
/NoneCS CS 20 w 1 SCN 0 100 m 200 100 l S";
let page = render(build(content, resources, &[]));
assert_near(
px(&page, 100.0, 100.0),
[255, 0, 0],
4,
"None stroke is invisible",
);
}
#[test]
fn all_none_devicen_paints_nothing() {
let func = stream_obj(
"/FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1 0 1]",
b"{ pop pop 0 0 0 1 }",
);
let resources = "/ColorSpace << /NN [/DeviceN [/None /None] /DeviceCMYK 5 0 R] >>";
let content = b"1 0 0 rg 0 0 200 200 re f\n\
/NN cs 0.5 0.5 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[func]));
assert_near(
px(&page, 100.0, 100.0),
[255, 0, 0],
4,
"all-None DeviceN is invisible",
);
}
#[test]
fn nchannel_none_component_adds_no_ink() {
let func = stream_obj(
"/FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1 0 1]",
b"{ 0 3 1 roll 0 }",
);
let gs = b"<< /Type /ExtGState /OP true /op true /OPM 1 >>".to_vec();
let resources = "/ColorSpace << /NC [/DeviceN [/Magenta /None] /DeviceCMYK 5 0 R] >> \
/ExtGState << /GS0 6 0 R >>";
let content = b"1 1 1 rg 0 0 200 200 re f\n\
/GS0 gs /NC cs 0.8 0.6 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[func, gs]));
let c = px(&page, 100.0, 100.0);
assert!(
c[2] > 180 && c[0] > 180 && c[1] < 120,
"magenta-only overprint should keep blue high (got {c:?})"
);
}
#[test]
fn nchannel_colorants_spot_overprints() {
let func = stream_obj(
"/FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1 0 1]",
b"{ pop pop 0 0 0 1 }",
);
let gs = b"<< /Type /ExtGState /OP true /op true /OPM 1 >>".to_vec();
let resources = "/ColorSpace << /NC [/DeviceN [/Cyan /MySpot] /DeviceCMYK 5 0 R \
<< /Subtype /NChannel /Colorants << /MySpot [/Separation /MySpot /DeviceCMYK \
<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 0 1 0] /N 1 >>] >> >>] >> \
/ExtGState << /GS0 6 0 R >>";
let content = b"1 1 1 rg 0 0 200 200 re f\n\
/GS0 gs /NC cs 1 1 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[func, gs]));
assert_near(
px(&page, 100.0, 100.0),
[0, 255, 0],
28,
"cyan + spot-yellow = green",
);
}
#[test]
fn nchannel_none_dropped_beside_unclassifiable_spot() {
let func = stream_obj(
"/FunctionType 4 /Domain [0 1 0 1] /Range [0 1 0 1 0 1 0 1]",
b"{ 0 3 1 roll 0 }",
);
let gs = b"<< /Type /ExtGState /OP true /op true /OPM 1 >>".to_vec();
let resources = "/ColorSpace << /NC [/DeviceN [/Spot1 /None] /DeviceCMYK 5 0 R] >> \
/ExtGState << /GS0 6 0 R >>";
let content = b"1 1 1 rg 0 0 200 200 re f\n\
/GS0 gs /NC cs 0.8 0.6 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[func, gs]));
let c = px(&page, 100.0, 100.0);
assert!(
c[2] > 180 && c[0] > 180 && c[1] < 120,
"isolated Spot1 should keep blue high (None dropped), got {c:?}"
);
}
#[test]
fn none_fill_suppresses_image_mask_stencil() {
let img = stream_obj(
"/Subtype /Image /Width 1 /Height 1 /ImageMask true /BitsPerComponent 1",
&[0x00],
);
let resources = "/ColorSpace << /NoneCS [/Separation /None /DeviceCMYK \
<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 0 0 1] /N 1 >>] >> \
/XObject << /Im 5 0 R >>";
let content = b"1 0 0 rg 0 0 200 200 re f\n\
/NoneCS cs 1 scn q 200 0 0 200 0 0 cm /Im Do Q";
let page = render(build(content, resources, &[img]));
assert_near(
px(&page, 100.0, 100.0),
[255, 0, 0],
4,
"None fill suppresses stencil mask",
);
}
#[test]
fn spot_separation_still_paints() {
let resources = "/ColorSpace << /Spot [/Separation /MySpot /DeviceCMYK \
<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 1 0 0] /N 1 >>] >>";
let content = b"1 1 1 rg 0 0 200 200 re f\n\
/Spot cs 1 scn 0 0 200 200 re f";
let page = render(build(content, resources, &[]));
let c = px(&page, 100.0, 100.0);
assert!(c[1] < 120, "spot magenta should paint (got {c:?})");
}