mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 02:57:34 -06:00
511 lines
24 KiB
Rust
511 lines
24 KiB
Rust
//! Test files are generated here (no third-party samples).
|
||
|
||
use super::*;
|
||
|
||
use crate::write::pdf;
|
||
|
||
const CONTENT: &str = "/OC /MC0 BDC\n0 1 0 rg 0 0 200 100 re f\nEMC\n\
|
||
/OC /MC1 BDC\nq 10 10 80 80 re W n\n1 0 0 rg 0 0 50 100 re f\nQ\n\
|
||
/Pattern cs /P0 scn 100 0 100 50 re f\n\
|
||
0 0 1 RG 4 w 120 80 m 180 80 l S\n\
|
||
BT /F1 12 Tf (ignored) Tj ET\nEMC\n";
|
||
|
||
pub(crate) fn sample_pdf(object_stream: bool) -> Vec<u8> {
|
||
let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Resources << /Properties << /MC0 5 0 R /MC1 6 0 R >> /Pattern << /P0 7 0 R >> >> /Contents 4 0 R >>";
|
||
let pattern = "<< /PatternType 2 /Shading << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [100 0 200 0] /Function << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> /Extend [true true] >> >>";
|
||
let mut objs = vec![
|
||
(1, "<< /Type /Catalog /Pages 2 0 R >>".to_string(), None),
|
||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(), None),
|
||
(3, page.to_string(), None),
|
||
(4, "<< >>".to_string(), Some(CONTENT.as_bytes().to_vec())),
|
||
(7, pattern.to_string(), None),
|
||
];
|
||
let ocg5 = "<< /Type /OCG /Name (Background) >>";
|
||
let ocg6 = "<< /Type /OCG /Name <FEFF00410072007400> >>"; // "Art" in UTF-16BE
|
||
if object_stream {
|
||
let body = format!("{ocg5} {ocg6}");
|
||
let head = format!("5 0 6 {} ", ocg5.len() + 1);
|
||
let data = format!("{head}{body}");
|
||
objs.push((8, format!("<< /Type /ObjStm /N 2 /First {} >>", head.len()), Some(data.into_bytes())));
|
||
} else {
|
||
objs.push((5, ocg5.to_string(), None));
|
||
objs.push((6, ocg6.to_string(), None));
|
||
}
|
||
pdf(&objs, 1)
|
||
}
|
||
|
||
fn px(doc: &Doc, x: i64, y: i64) -> [f32; 4] {
|
||
let (w, h) = doc.pixel_size();
|
||
let img = effectcraft_svg::rasterize(doc, w, h, 1.0);
|
||
img.get(x, y)
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_paths_clip_gradient_and_layers() {
|
||
for objstm in [false, true] {
|
||
let bytes = sample_pdf(objstm);
|
||
assert_eq!(sniff(&bytes), Some(Format::Pdf));
|
||
assert_eq!(page_count(&bytes), 1);
|
||
let doc = parse(&bytes).unwrap();
|
||
assert_eq!((doc.width, doc.height), (200.0, 100.0));
|
||
assert_eq!(layer_names(&doc), vec!["Background", "Art"], "object stream {objstm}");
|
||
assert!(doc.skipped.contains(&"text (no font)".to_string()), "{:?}", doc.skipped);
|
||
let (w, h) = doc.pixel_size();
|
||
let img = effectcraft_svg::rasterize(&doc, w, h, 1.0);
|
||
let at = |x: i64, y: i64| img.get(x, y);
|
||
// Red rectangle clipped to x ≥ 10, over the green background.
|
||
assert!(at(30, 50)[0] > 0.99 && at(30, 50)[1] < 0.01, "{:?}", at(30, 50));
|
||
assert!(at(5, 50)[1] > 0.99 && at(5, 50)[0] < 0.01, "clipped out: {:?}", at(5, 50));
|
||
assert!(at(70, 50)[1] > 0.99, "{:?}", at(70, 50));
|
||
// Axial shading, red → blue left to right (in the lower half: PDF y is up).
|
||
let (l, r) = (at(110, 75), at(190, 75));
|
||
assert!(l[0] > 0.8 && l[2] < 0.2 && r[2] > 0.8 && r[0] < 0.2, "{l:?} {r:?}");
|
||
assert!(at(150, 25)[1] > 0.99, "the gradient stays in its rectangle: {:?}", at(150, 25));
|
||
// Stroked line (PDF y = 80 → 20 px from the top).
|
||
assert!(at(150, 20)[2] > 0.99 && at(150, 20)[1] < 0.01, "{:?}", at(150, 20));
|
||
// One layer at a time.
|
||
let bg = layer_doc(&doc, 0);
|
||
assert!(px(&bg, 30, 50)[1] > 0.99);
|
||
let art = layer_doc(&doc, 1);
|
||
assert_eq!(px(&art, 5, 50)[3], 0.0);
|
||
// Continuous rasterisation: twice the size stays sharp at the clip edge.
|
||
let big = effectcraft_svg::rasterize(&doc, 400, 200, 2.0);
|
||
assert!(big.get(21, 100)[0] > 0.99 && big.get(18, 100)[1] > 0.99);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn pdf_forms_rotation_cmyk_and_alpha() {
|
||
let content = "q 2 0 0 2 0 0 cm /Fm0 Do Q\n/GS0 gs 0 0 0 1 k 0 0 10 10 re f\n";
|
||
let form = "0 1 1 0 k 0 0 20 20 re f";
|
||
let objs = vec![
|
||
(1, "<< /Type /Catalog /Pages 2 0 R >>".to_string(), None),
|
||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 /Rotate 90 >>".to_string(), None),
|
||
(
|
||
3,
|
||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /Resources << /XObject << /Fm0 5 0 R >> /ExtGState << /GS0 << /ca 0.5 >> >> >> /Contents 4 0 R >>"
|
||
.to_string(),
|
||
None,
|
||
),
|
||
(4, "<< >>".to_string(), Some(content.as_bytes().to_vec())),
|
||
(5, "<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] >>".to_string(), Some(form.as_bytes().to_vec())),
|
||
];
|
||
let doc = parse(&pdf(&objs, 1)).unwrap();
|
||
// Rotated a quarter turn: 50 × 100.
|
||
assert_eq!((doc.width, doc.height), (50.0, 100.0));
|
||
let (w, h) = doc.pixel_size();
|
||
let img = effectcraft_svg::rasterize(&doc, w, h, 1.0);
|
||
// The form (CMYK red, clipped to its 10×10 box, scaled ×2) covers PDF (0..20, 0..20); the
|
||
// half-transparent black square covers PDF (0..10, 0..10). /Rotate 90: PDF (x, y) → (y, x).
|
||
let red = img.get(15, 15);
|
||
assert!(red[0] > 0.99 && red[1] < 0.01 && red[3] > 0.99, "{red:?}");
|
||
let dark = img.get(5, 5);
|
||
assert!((dark[0] - 0.5).abs() < 0.02 && dark[3] > 0.99, "{dark:?}");
|
||
assert_eq!(img.get(30, 30)[3], 0.0, "form clipped to its bounding box");
|
||
}
|
||
|
||
const EPS: &str = "%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n\
|
||
/m {moveto} bind def /l {lineto} bind def\n\
|
||
/cm { 6 array astore concat } bind def\n\
|
||
/box { 4 dict begin /h exch def /w exch def /y exch def /x exch def x y m w 0 rlineto 0 h rlineto w neg 0 rlineto closepath end } bind def\n\
|
||
1 0 0 setrgbcolor\n10 10 30 30 box fill\n\
|
||
gsave 0 0 1 setrgbcolor 70 50 20 0 360 arc fill grestore\n\
|
||
gsave 1 0 0 1 50 0 cm 0 1 0 setrgbcolor 0 0 10 10 box fill grestore\n\
|
||
0 setgray 2 setlinewidth 0 90 m 100 90 l stroke\n\
|
||
/Helvetica findfont 12 scalefont setfont 5 5 moveto (text) show\n\
|
||
showpage\n%%EOF\n";
|
||
|
||
#[test]
|
||
fn eps_postscript_subset() {
|
||
let doc = parse(EPS.as_bytes()).unwrap();
|
||
assert_eq!((doc.width, doc.height), (100.0, 100.0));
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
assert!(shape_names(&doc).contains(&"Text: text".to_string()), "EPS text is drawn: {:?}", shape_names(&doc));
|
||
let at = |x, y| px(&doc, x, y);
|
||
assert!(at(25, 75)[0] > 0.99 && at(25, 75)[3] > 0.99, "red box {:?}", at(25, 75));
|
||
assert!(at(70, 50)[2] > 0.99, "blue disc {:?}", at(70, 50));
|
||
assert!(at(55, 95)[1] > 0.99, "translated green box {:?}", at(55, 95));
|
||
let line = at(50, 10);
|
||
assert!(line[3] > 0.99 && line[0] < 0.01, "black line {line:?}");
|
||
assert_eq!(at(95, 70)[3], 0.0);
|
||
// DOS EPS binary header (with a fake TIFF preview after the PostScript).
|
||
let ps = EPS.as_bytes();
|
||
let mut dos = vec![0xC5, 0xD0, 0xD3, 0xC6];
|
||
dos.extend_from_slice(&30u32.to_le_bytes());
|
||
dos.extend_from_slice(&(ps.len() as u32).to_le_bytes());
|
||
dos.extend_from_slice(&[0; 18]);
|
||
dos.extend_from_slice(ps);
|
||
dos.extend_from_slice(b"II*\0 not a real preview");
|
||
let d2 = parse(&dos).unwrap();
|
||
assert_eq!(d2.root.children.len(), doc.root.children.len());
|
||
assert_eq!(codec("art.eps", &dos), Some("EPS"));
|
||
assert_eq!(codec("art.ai", &sample_pdf(false)), Some("AI"));
|
||
assert_eq!(parse(b"hello"), Err(Error::NotVector));
|
||
}
|
||
|
||
#[test]
|
||
fn filters_decode() {
|
||
assert_eq!(object::ascii85(b"<~87cURD]i,\"Ebo80~>"), b"Hello World!".to_vec());
|
||
let z = miniz_oxide::deflate::compress_to_vec_zlib(b"abc", 6);
|
||
assert_eq!(object::inflate(&z).unwrap(), b"abc");
|
||
}
|
||
|
||
// ------------------------------------------------------------------ M13.6: text, images,
|
||
// patterns, soft masks, blend modes, pages
|
||
|
||
pub(crate) type Objs = Vec<(u32, String, Option<Vec<u8>>)>;
|
||
|
||
/// A one-page 200×100 PDF: `resources` is the page's resource dictionary body, `extra` more
|
||
/// objects (numbered from 10).
|
||
pub(crate) fn page_pdf(content: &str, resources: &str, extra: Objs) -> Vec<u8> {
|
||
let mut objs: Objs = vec![
|
||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
|
||
(3, format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Resources << {resources} >> /Contents 4 0 R >>"), None),
|
||
(4, "<< >>".into(), Some(content.as_bytes().to_vec())),
|
||
];
|
||
objs.extend(extra);
|
||
pdf(&objs, 1)
|
||
}
|
||
|
||
pub(crate) fn render(doc: &Doc) -> effectcraft_raster::Image {
|
||
let (w, h) = doc.pixel_size();
|
||
effectcraft_svg::rasterize(doc, w, h, 1.0)
|
||
}
|
||
|
||
pub(crate) fn coverage(img: &effectcraft_raster::Image, x0: i64, y0: i64, x1: i64, y1: i64) -> f32 {
|
||
let mut s = 0.0;
|
||
for y in y0..y1 {
|
||
for x in x0..x1 {
|
||
s += img.get(x, y)[3];
|
||
}
|
||
}
|
||
s
|
||
}
|
||
|
||
pub(crate) fn shape_names(doc: &Doc) -> Vec<String> {
|
||
fn walk(g: &effectcraft_svg::Group, out: &mut Vec<String>) {
|
||
for c in &g.children {
|
||
match c {
|
||
Node::Group(s) => walk(s, out),
|
||
Node::Shape(s) => out.push(s.name.clone()),
|
||
Node::Image(i) => out.push(i.name.clone()),
|
||
}
|
||
}
|
||
}
|
||
let mut out = vec![];
|
||
walk(&doc.root, &mut out);
|
||
out
|
||
}
|
||
|
||
/// Type 2 charstring operands.
|
||
fn t2(v: &[i32]) -> Vec<u8> {
|
||
let mut o = vec![];
|
||
for &x in v {
|
||
if (-107..=107).contains(&x) {
|
||
o.push((x + 139) as u8);
|
||
} else {
|
||
o.push(28);
|
||
o.extend_from_slice(&(x as i16).to_be_bytes());
|
||
}
|
||
}
|
||
o
|
||
}
|
||
|
||
/// A CFF font with a 500-unit square glyph `square` (x 50..550, y 0..500, width 600).
|
||
fn square_cff() -> Vec<u8> {
|
||
let mut sq = t2(&[600, 50, 0]);
|
||
sq.push(21);
|
||
sq.extend(t2(&[500, 500, -500]));
|
||
sq.push(6);
|
||
sq.push(14);
|
||
crate::cff::write_test_cff(&[(".notdef", vec![14]), ("square", sq)])
|
||
}
|
||
|
||
const SQUARE_FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Square /FirstChar 65 /LastChar 65 /Widths [600] /Encoding << /Differences [65 /square] >> /FontDescriptor 11 0 R >>";
|
||
|
||
pub(crate) fn square_font_objs() -> Objs {
|
||
vec![
|
||
(10, SQUARE_FONT.into(), None),
|
||
(11, "<< /Type /FontDescriptor /FontName /Square /Flags 32 /FontFile3 12 0 R >>".into(), None),
|
||
(12, "<< /Subtype /Type1C >>".into(), Some(square_cff())),
|
||
]
|
||
}
|
||
|
||
#[test]
|
||
fn text_with_an_embedded_cff_font_spacing_and_tj() {
|
||
// Squares 10 pt per 100 units: A at 0..60 (square 5..55), Tc 10 and a TJ kern of −400
|
||
// (+40) put the second at 110 (square 115..165).
|
||
let bytes = page_pdf("BT /F1 100 Tf 0 0 Td 10 Tc [(A) -400 (A)] TJ ET", "/Font << /F1 10 0 R >>", square_font_objs());
|
||
let doc = parse(&bytes).unwrap();
|
||
assert!(!doc.skipped.iter().any(|s| s.contains("text")), "{:?}", doc.skipped);
|
||
assert!(shape_names(&doc).iter().any(|n| n.starts_with("Text")), "{:?}", shape_names(&doc));
|
||
let img = render(&doc);
|
||
for (x, on) in [(30, true), (60, false), (105, false), (140, true), (170, false)] {
|
||
assert_eq!(img.get(x, 75)[3] > 0.99, on, "x = {x}: {:?}", img.get(x, 75));
|
||
}
|
||
// Squares are 50 pt tall: rows 50..100.
|
||
assert_eq!(img.get(30, 45)[3], 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn text_render_mode_clip() {
|
||
let bytes = page_pdf("BT /F1 100 Tf 7 Tr 0 0 Td (A) Tj ET 1 0 0 rg 0 0 200 100 re f", "/Font << /F1 10 0 R >>", square_font_objs());
|
||
let img = render(&parse(&bytes).unwrap());
|
||
assert!(img.get(30, 75)[0] > 0.99 && img.get(30, 75)[3] > 0.99);
|
||
assert_eq!(img.get(100, 75)[3], 0.0, "clipped to the glyph");
|
||
assert_eq!(img.get(30, 25)[3], 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn text_with_an_embedded_type1_font() {
|
||
// hsbw 50 600; 0 0 rmoveto; 500 hlineto 500 vlineto -500 hlineto closepath endchar
|
||
let cs = vec![50 + 139, 248, 236, 13, 139, 139, 21, 248, 136, 6, 248, 136, 7, 252, 136, 6, 9, 14];
|
||
let font = crate::type1::write_test_type1(&[(".notdef", vec![139, 139, 13, 14]), ("box", cs)], &[(66, "box")]);
|
||
let extra: Objs = vec![
|
||
(10, "<< /Type /Font /Subtype /Type1 /BaseFont /Box /FirstChar 66 /LastChar 66 /Widths [600] /FontDescriptor 11 0 R >>".into(), None),
|
||
(11, "<< /Type /FontDescriptor /FontName /Box /Flags 4 /FontFile 12 0 R >>".into(), None),
|
||
(12, "<< /Length1 0 /Length2 0 /Length3 0 >>".into(), Some(font)),
|
||
];
|
||
// The font's built-in encoding maps B (66) to `box`.
|
||
let img = render(&parse(&page_pdf("BT /F1 100 Tf 0 0 Td (BB) Tj ET", "/Font << /F1 10 0 R >>", extra)).unwrap());
|
||
for (x, on) in [(30, true), (60, false), (90, true), (120, false)] {
|
||
assert_eq!(img.get(x, 75)[3] > 0.99, on, "x = {x}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn text_with_a_type3_font() {
|
||
let extra: Objs = vec![
|
||
(
|
||
10,
|
||
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 100 100] /FontMatrix [0.01 0 0 0.01 0 0] /CharProcs << /sq 11 0 R >> /Encoding << /Type /Encoding /Differences [65 /sq] >> /FirstChar 65 /LastChar 65 /Widths [100] >>".into(),
|
||
None,
|
||
),
|
||
(11, "<< >>".into(), Some(b"100 0 d0 0 0 100 100 re f".to_vec())),
|
||
];
|
||
let img = render(&parse(&page_pdf("BT 0 0 1 rg /F1 20 Tf 10 10 Td (AA) Tj ET", "/Font << /F1 10 0 R >>", extra)).unwrap());
|
||
// 20 pt squares from x = 10, advancing 20: user 10..30 and 30..50, y 10..30 (rows 70..90).
|
||
assert!(img.get(20, 80)[2] > 0.99 && img.get(20, 80)[3] > 0.99, "{:?}", img.get(20, 80));
|
||
assert!(img.get(40, 80)[3] > 0.99);
|
||
assert_eq!(img.get(55, 80)[3], 0.0);
|
||
assert_eq!(img.get(20, 60)[3], 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn text_with_standard_fonts_embedded_truetype_and_cid_fonts() {
|
||
use skrifa::MetadataProvider;
|
||
let inter = effectcraft_text::fonts::INTER_REGULAR.to_vec();
|
||
// Not embedded: Helvetica drawn with the bundled sans serif.
|
||
let std14 = page_pdf("BT /F1 60 Tf 10 30 Td (Hi) Tj ET", "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>", vec![]);
|
||
let doc = parse(&std14).unwrap();
|
||
assert!(shape_names(&doc).contains(&"Text: Hi".to_string()), "{:?}", shape_names(&doc));
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
let a = render(&doc);
|
||
let ink = coverage(&a, 0, 0, 200, 100);
|
||
assert!(ink > 300.0, "{ink}");
|
||
// Ink only on the text line: baseline at row 70, cap height ≈ 44 px above it.
|
||
assert!(coverage(&a, 0, 72, 200, 100) < 1.0 && coverage(&a, 0, 0, 200, 20) < 1.0);
|
||
// The same face embedded (a TrueType font program, WinAnsiEncoding): the same outlines.
|
||
let tt = page_pdf(
|
||
"BT /F1 60 Tf 10 30 Td (Hi) Tj ET",
|
||
"/Font << /F1 10 0 R >>",
|
||
vec![
|
||
(10, "<< /Type /Font /Subtype /TrueType /BaseFont /ABCDEF+Inter /Encoding /WinAnsiEncoding /FontDescriptor 11 0 R >>".into(), None),
|
||
(11, "<< /Type /FontDescriptor /Flags 32 /FontFile2 12 0 R >>".into(), None),
|
||
(12, "<< >>".into(), Some(inter.clone())),
|
||
],
|
||
);
|
||
let b = render(&parse(&tt).unwrap());
|
||
assert!((coverage(&b, 0, 0, 200, 100) - ink).abs() < 1.0, "{} vs {ink}", coverage(&b, 0, 0, 200, 100));
|
||
// A composite font: Identity-H codes are glyph ids of a CIDFontType2.
|
||
let font = skrifa::FontRef::new(&inter).unwrap();
|
||
let gid = |c: char| font.charmap().map(c).unwrap().to_u32() as u16;
|
||
let (h, i) = (gid('H'), gid('i'));
|
||
let loc = skrifa::instance::LocationRef::default();
|
||
let upem = font.metrics(skrifa::instance::Size::unscaled(), loc).units_per_em as f32;
|
||
let w = |g: u16| font.glyph_metrics(skrifa::instance::Size::unscaled(), loc).advance_width(skrifa::GlyphId::new(g as u32)).unwrap() / upem * 1000.0;
|
||
let cid = page_pdf(
|
||
&format!("BT /F1 60 Tf 10 30 Td <{h:04X}{i:04X}> Tj ET"),
|
||
"/Font << /F1 10 0 R >>",
|
||
vec![
|
||
(10, "<< /Type /Font /Subtype /Type0 /BaseFont /Inter /Encoding /Identity-H /DescendantFonts [13 0 R] >>".into(), None),
|
||
(
|
||
13,
|
||
format!(
|
||
"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Inter /CIDToGIDMap /Identity /W [{h} [{}] {i} [{}]] /FontDescriptor 11 0 R >>",
|
||
w(h),
|
||
w(i)
|
||
),
|
||
None,
|
||
),
|
||
(11, "<< /Type /FontDescriptor /Flags 32 /FontFile2 12 0 R >>".into(), None),
|
||
(12, "<< >>".into(), Some(inter)),
|
||
],
|
||
);
|
||
let c = render(&parse(&cid).unwrap());
|
||
assert!((coverage(&c, 0, 0, 200, 100) - ink).abs() < 1.0, "{} vs {ink}", coverage(&c, 0, 0, 200, 100));
|
||
}
|
||
|
||
#[test]
|
||
fn images_flate_smask_indexed_stencil_inline_and_dct() {
|
||
let objs = |im: &str, data: Vec<u8>, extra: Objs| -> Vec<u8> {
|
||
let mut e: Objs = vec![(10, im.to_string(), Some(data))];
|
||
e.extend(extra);
|
||
page_pdf("q 100 0 0 50 0 0 cm /Im1 Do Q", "/XObject << /Im1 10 0 R >>", e)
|
||
};
|
||
// RGB with a soft mask: red opaque, green transparent.
|
||
let doc = parse(&objs(
|
||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /SMask 11 0 R >>",
|
||
vec![255, 0, 0, 0, 255, 0],
|
||
vec![(11, "<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 >>".into(), Some(vec![255, 0]))],
|
||
))
|
||
.unwrap();
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
let img = render(&doc);
|
||
assert!(img.get(25, 75)[0] > 0.95 && img.get(25, 75)[3] > 0.95, "{:?}", img.get(25, 75));
|
||
assert!(img.get(75, 75)[3] < 0.05, "{:?}", img.get(75, 75));
|
||
assert_eq!(img.get(25, 25)[3], 0.0, "the image is the unit square of user space");
|
||
// Indexed (palette in a string), 8 bits.
|
||
let img = render(
|
||
&parse(&objs(
|
||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace [/Indexed /DeviceRGB 1 <0000FFFFFF00>] /BitsPerComponent 8 >>",
|
||
vec![1, 0],
|
||
vec![],
|
||
))
|
||
.unwrap(),
|
||
);
|
||
let (l, r) = (img.get(25, 75), img.get(75, 75));
|
||
assert!(l[0] > 0.95 && l[1] > 0.95 && l[2] < 0.05 && r[2] > 0.95 && r[0] < 0.05, "{l:?} {r:?}");
|
||
// ICCBased with an /Alternate space, 1-bit samples.
|
||
let img = render(
|
||
&parse(&objs(
|
||
"<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ColorSpace [/ICCBased 11 0 R] /BitsPerComponent 1 >>",
|
||
vec![0b0100_0000],
|
||
vec![(11, "<< /N 1 /Alternate /DeviceGray >>".into(), Some(vec![0; 16]))],
|
||
))
|
||
.unwrap(),
|
||
);
|
||
assert!(img.get(25, 75)[0] < 0.05 && img.get(75, 75)[0] > 0.95);
|
||
// Stencil mask painted in the fill colour (sample 0 paints).
|
||
let stencil = page_pdf(
|
||
"0 1 0 rg q 100 0 0 50 0 0 cm /Im1 Do Q",
|
||
"/XObject << /Im1 10 0 R >>",
|
||
vec![(10, "<< /Type /XObject /Subtype /Image /Width 2 /Height 1 /ImageMask true >>".into(), Some(vec![0b0100_0000]))],
|
||
);
|
||
let img = render(&parse(&stencil).unwrap());
|
||
assert!(img.get(25, 75)[1] > 0.95 && img.get(25, 75)[3] > 0.95);
|
||
assert!(img.get(75, 75)[3] < 0.05);
|
||
// Inline image (abbreviated keys, ASCIIHex) in the top half.
|
||
let inline = page_pdf("q 100 0 0 50 0 50 cm BI /W 2 /H 1 /CS /RGB /BPC 8 /F /AHx ID 00FF00FF0000> EI Q 0 0 1 rg 150 0 50 50 re f", "", vec![]);
|
||
let doc = parse(&inline).unwrap();
|
||
let img = render(&doc);
|
||
assert!(img.get(25, 25)[1] > 0.95 && img.get(75, 25)[0] > 0.95, "{:?} {:?}", img.get(25, 25), img.get(75, 25));
|
||
assert!(img.get(175, 75)[2] > 0.95, "content after EI still runs");
|
||
// DCT (JPEG): left half red, right half blue.
|
||
let mut jpg = vec![];
|
||
let mut rgb = ::image::RgbImage::new(16, 16);
|
||
for (x, _, p) in rgb.enumerate_pixels_mut() {
|
||
*p = if x < 8 { ::image::Rgb([255, 0, 0]) } else { ::image::Rgb([0, 0, 255]) };
|
||
}
|
||
::image::DynamicImage::ImageRgb8(rgb).write_to(&mut std::io::Cursor::new(&mut jpg), ::image::ImageFormat::Jpeg).unwrap();
|
||
let doc =
|
||
parse(&objs("<< /Type /XObject /Subtype /Image /Width 16 /Height 16 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode >>", jpg, vec![]))
|
||
.unwrap();
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
let img = render(&doc);
|
||
let (l, r) = (img.get(20, 75), img.get(80, 75));
|
||
assert!(l[0] > 0.85 && l[2] < 0.15 && r[2] > 0.85 && r[0] < 0.15, "{l:?} {r:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn soft_masks_and_blend_modes() {
|
||
// Luminosity: a form that is white on the left half, transparent (→ black backdrop) elsewhere.
|
||
let lum = page_pdf(
|
||
"q /GS1 gs 1 0 0 rg 0 0 200 100 re f Q 0 0 1 rg 0 0 20 20 re f",
|
||
"/ExtGState << /GS1 << /SMask << /S /Luminosity /G 10 0 R >> >> >>",
|
||
vec![(
|
||
10,
|
||
"<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Group << /S /Transparency /CS /DeviceGray >> >>".into(),
|
||
Some(b"1 g 0 0 100 100 re f".to_vec()),
|
||
)],
|
||
);
|
||
let doc = parse(&lum).unwrap();
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
let img = render(&doc);
|
||
assert!(img.get(50, 50)[0] > 0.99 && img.get(50, 50)[3] > 0.99, "{:?}", img.get(50, 50));
|
||
assert!(img.get(150, 50)[3] < 0.01, "{:?}", img.get(150, 50));
|
||
assert!(img.get(10, 90)[2] > 0.99, "the mask ends with Q");
|
||
// Alpha: the mask form's coverage.
|
||
let alpha = page_pdf(
|
||
"/GS1 gs 1 0 0 rg 0 0 200 100 re f",
|
||
"/ExtGState << /GS1 << /SMask << /S /Alpha /G 10 0 R >> >> >>",
|
||
vec![(10, "<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] >>".into(), Some(b"0 0 0 rg 100 0 100 100 re f".to_vec()))],
|
||
);
|
||
let img = render(&parse(&alpha).unwrap());
|
||
assert!(img.get(50, 50)[3] < 0.01 && img.get(150, 50)[0] > 0.99);
|
||
// Multiply over a light blue backdrop; Screen in the right half.
|
||
let blend = page_pdf(
|
||
"0.5 0.5 1 rg 0 0 200 100 re f q /M gs 1 1 0 rg 0 0 100 100 re f Q q /S gs 0.5 0 0 rg 100 0 100 100 re f Q",
|
||
"/ExtGState << /M << /BM /Multiply >> /S << /BM [/Screen] >> >>",
|
||
vec![],
|
||
);
|
||
let img = render(&parse(&blend).unwrap());
|
||
let m = img.get(50, 50);
|
||
assert!((m[0] - 0.5).abs() < 0.01 && (m[1] - 0.5).abs() < 0.01 && m[2] < 0.01, "multiply {m:?}");
|
||
let s = img.get(150, 50);
|
||
assert!((s[0] - 0.75).abs() < 0.01 && (s[1] - 0.5).abs() < 0.01 && (s[2] - 1.0).abs() < 0.01, "screen {s:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn tiling_patterns_coloured_and_uncoloured() {
|
||
let cell = b"1 0 0 rg 0 0 10 10 re f 0 0 1 rg 10 10 10 10 re f".to_vec();
|
||
let bytes = page_pdf(
|
||
"/Pattern cs /P1 scn 0 0 100 100 re f /Cs1 cs 0 1 0 /P2 scn 100 0 100 100 re f",
|
||
"/Pattern << /P1 10 0 R /P2 11 0 R >> /ColorSpace << /Cs1 [/Pattern /DeviceRGB] >>",
|
||
vec![
|
||
(10, "<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 /Resources << >> >>".into(), Some(cell)),
|
||
(
|
||
11,
|
||
"<< /Type /Pattern /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 20 20] /XStep 20 /YStep 20 >>".into(),
|
||
Some(b"1 0 0 rg 0 0 10 10 re f".to_vec()),
|
||
),
|
||
],
|
||
);
|
||
let doc = parse(&bytes).unwrap();
|
||
assert!(doc.skipped.is_empty(), "{:?}", doc.skipped);
|
||
let img = render(&doc);
|
||
// PDF (x, y) → pixel (x, 100 − y).
|
||
let at = |x: i64, y: i64| img.get(x, 100 - y);
|
||
assert!(at(5, 5)[0] > 0.99 && at(25, 5)[0] > 0.99 && at(85, 45)[0] > 0.99, "{:?}", at(5, 5));
|
||
assert!(at(15, 15)[2] > 0.99 && at(55, 35)[2] > 0.99);
|
||
assert_eq!(at(15, 5)[3], 0.0);
|
||
// Uncoloured: the cell in the colour given with the pattern (green), its own colour ignored.
|
||
assert!(at(105, 5)[1] > 0.99 && at(105, 5)[0] < 0.01, "{:?}", at(105, 5));
|
||
assert_eq!(at(115, 15)[3], 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn multiple_pages_and_calculator_shadings() {
|
||
let objs: Objs = vec![
|
||
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
|
||
(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>".into(), None),
|
||
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] /Contents 4 0 R >>".into(), None),
|
||
(4, "<< >>".into(), Some(b"1 0 0 rg 0 0 200 100 re f".to_vec())),
|
||
(5, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 50] /Resources << /Shading << /Sh 7 0 R >> >> /Contents 6 0 R >>".into(), None),
|
||
(6, "<< >>".into(), Some(b"/Sh sh".to_vec())),
|
||
(7, "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function 8 0 R >>".into(), None),
|
||
(8, "<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] >>".into(), Some(b"{ 0 0 }".to_vec())),
|
||
];
|
||
let bytes = pdf(&objs, 1);
|
||
assert_eq!(page_count(&bytes), 2);
|
||
let p1 = parse_page(&bytes, 0).unwrap();
|
||
assert_eq!((p1.width, p1.height), (200.0, 100.0));
|
||
let p2 = parse_page(&bytes, 1).unwrap();
|
||
assert_eq!((p2.width, p2.height), (100.0, 50.0));
|
||
assert!(p2.skipped.is_empty(), "{:?}", p2.skipped);
|
||
let img = render(&p2);
|
||
assert!(img.get(5, 25)[0] < 0.1 && img.get(95, 25)[0] > 0.9, "{:?} {:?}", img.get(5, 25), img.get(95, 25));
|
||
assert_eq!(parse_page(&bytes, 2), Err(Error::PageOutOfRange(2)));
|
||
}
|