223 lines
7.7 KiB
Rust
223 lines
7.7 KiB
Rust
//! 1:1 port of `dart-pdf/packages/pdf_graphics/test/shading_test.dart` (first tranche)
|
|
//! Covers PdfFunction type 2 (exponential) and type 3 (stitching).
|
|
|
|
use nigig_pdf_cos::{PdfDict, PdfObj};
|
|
use nigig_pdf_graphics::function::PdfFunction;
|
|
|
|
fn dict(pairs: Vec<(&str, PdfObj)>) -> PdfDict {
|
|
let mut d = PdfDict::new();
|
|
for (k, v) in pairs {
|
|
d.set(k, v);
|
|
}
|
|
d
|
|
}
|
|
|
|
fn array(vals: Vec<PdfObj>) -> PdfObj {
|
|
PdfObj::Array(vals)
|
|
}
|
|
|
|
fn exponential(c0: Vec<f64>, c1: Vec<f64>) -> PdfDict {
|
|
let mut d = PdfDict::new();
|
|
d.set("FunctionType", PdfObj::Int(2));
|
|
d.set("Domain", array(vec![PdfObj::Int(0), PdfObj::Int(1)]));
|
|
d.set("C0", array(c0.into_iter().map(PdfObj::Real).collect()));
|
|
d.set("C1", array(c1.into_iter().map(PdfObj::Real).collect()));
|
|
d.set("N", PdfObj::Int(1));
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn type2_interpolates_exponentially() {
|
|
let d = exponential(vec![1.0, 0.0, 0.0], vec![0.0, 0.0, 1.0]);
|
|
let func = PdfFunction::parse(&d, None, &mut |_: &PdfObj| None).expect("parse type2");
|
|
let v0 = func.eval(&[0.0]).expect("eval 0");
|
|
assert!((v0[0] - 1.0).abs() < 1e-9 && (v0[2] - 0.0).abs() < 1e-9, "got {v0:?}");
|
|
let v1 = func.eval(&[1.0]).expect("eval 1");
|
|
assert!((v1[0] - 0.0).abs() < 1e-9 && (v1[2] - 1.0).abs() < 1e-9, "got {v1:?}");
|
|
let v05 = func.eval(&[0.5]).expect("eval 0.5");
|
|
assert!((v05[0] - 0.5).abs() < 1e-9 && (v05[2] - 0.5).abs() < 1e-9, "got {v05:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn type3_stitches_subfunctions() {
|
|
let mut d = PdfDict::new();
|
|
d.set("FunctionType", PdfObj::Int(3));
|
|
d.set("Domain", array(vec![PdfObj::Int(0), PdfObj::Int(1)]));
|
|
d.set(
|
|
"Functions",
|
|
array(vec![
|
|
PdfObj::Dict(exponential(vec![0.0, 0.0, 0.0], vec![1.0, 1.0, 1.0])),
|
|
PdfObj::Dict(exponential(vec![1.0, 1.0, 1.0], vec![0.0, 0.0, 0.0])),
|
|
]),
|
|
);
|
|
d.set("Bounds", array(vec![PdfObj::Real(0.5)]));
|
|
d.set(
|
|
"Encode",
|
|
array(vec![PdfObj::Int(0), PdfObj::Int(1), PdfObj::Int(0), PdfObj::Int(1)]),
|
|
);
|
|
// Smoke test: type3 with two subfunctions should parse and evaluate without panic
|
|
let func = PdfFunction::parse(&d, None, &mut |o: &PdfObj| {
|
|
o.as_dict().map(|d| (d.clone(), None))
|
|
});
|
|
assert!(func.is_ok(), "type3 parse should not panic, got {func:?}");
|
|
if let Ok(f) = func {
|
|
let v = f.eval(&[0.25]).expect("eval 0.25");
|
|
assert_eq!(v.len(), 3);
|
|
}
|
|
}
|
|
|
|
// ---------- type 0 (sampled) ----------
|
|
|
|
fn sample_stream_1d(samples: Vec<u8>) -> (PdfDict, Vec<u8>) {
|
|
let mut d = PdfDict::new();
|
|
d.set("FunctionType", PdfObj::Int(0));
|
|
d.set("Domain", array(vec![PdfObj::Int(0), PdfObj::Int(1)]));
|
|
d.set("Range", array(vec![PdfObj::Int(0), PdfObj::Int(1)]));
|
|
d.set("Size", array(vec![PdfObj::Int(2)]));
|
|
d.set("BitsPerSample", PdfObj::Int(8));
|
|
(d, samples)
|
|
}
|
|
|
|
#[test]
|
|
fn type0_interpolates_linearly_between_samples() {
|
|
let (d, data) = sample_stream_1d(vec![0, 255]);
|
|
let f = PdfFunction::parse(&d, Some(&data), &mut |_: &PdfObj| None).expect("parse type0");
|
|
assert_eq!(f.eval(&[0.0]).expect("e0"), vec![0.0]);
|
|
assert_eq!(f.eval(&[1.0]).expect("e1"), vec![1.0]);
|
|
assert!((f.eval(&[0.5]).expect("e05")[0] - 0.5).abs() < 1e-9);
|
|
assert!((f.eval(&[0.25]).expect("e025")[0] - 0.25).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn type0_evaluates_every_input_of_a_multidimensional_sampler() {
|
|
// A 2x2x2 identity tint transform: 3 inputs -> 3 outputs, first
|
|
// dimension varying fastest.
|
|
let mut samples = Vec::new();
|
|
for i2 in 0..2 {
|
|
for i1 in 0..2 {
|
|
for i0 in 0..2 {
|
|
samples.extend([i0 * 255, i1 * 255, i2 * 255]);
|
|
}
|
|
}
|
|
}
|
|
let mut d = PdfDict::new();
|
|
d.set("FunctionType", PdfObj::Int(0));
|
|
let mut domain = Vec::new();
|
|
let mut range = Vec::new();
|
|
for _ in 0..3 {
|
|
domain.extend([PdfObj::Int(0), PdfObj::Int(1)]);
|
|
range.extend([PdfObj::Int(0), PdfObj::Int(1)]);
|
|
}
|
|
d.set("Domain", array(domain));
|
|
d.set("Range", array(range));
|
|
d.set(
|
|
"Size",
|
|
array(vec![PdfObj::Int(2), PdfObj::Int(2), PdfObj::Int(2)]),
|
|
);
|
|
d.set("BitsPerSample", PdfObj::Int(8));
|
|
let f = PdfFunction::parse(&d, Some(&samples), &mut |_: &PdfObj| None).expect("parse");
|
|
let close = |got: Vec<f64>, want: &[f64]| {
|
|
assert_eq!(got.len(), want.len());
|
|
for (g, w) in got.iter().zip(want) {
|
|
assert!((g - w).abs() < 1e-9, "got {got:?} want {want:?}");
|
|
}
|
|
};
|
|
close(f.eval(&[1.0, 0.0, 0.0]).expect("e"), &[1.0, 0.0, 0.0]);
|
|
close(f.eval(&[0.0, 1.0, 0.0]).expect("e"), &[0.0, 1.0, 0.0]);
|
|
close(f.eval(&[0.0, 0.0, 1.0]).expect("e"), &[0.0, 0.0, 1.0]);
|
|
close(f.eval(&[1.0, 1.0, 0.0]).expect("e"), &[1.0, 1.0, 0.0]);
|
|
close(f.eval(&[0.5, 0.25, 0.75]).expect("e"), &[0.5, 0.25, 0.75]);
|
|
}
|
|
|
|
// ---------- type 4 (PostScript calculator) ----------
|
|
|
|
fn calculator(program: &str, range: Vec<PdfObj>) -> (PdfDict, Vec<u8>) {
|
|
let mut d = PdfDict::new();
|
|
d.set("FunctionType", PdfObj::Int(4));
|
|
d.set("Domain", array(vec![PdfObj::Int(0), PdfObj::Int(1)]));
|
|
d.set("Range", array(range));
|
|
(d, program.as_bytes().to_vec())
|
|
}
|
|
|
|
fn unit_range() -> Vec<PdfObj> {
|
|
vec![PdfObj::Int(0), PdfObj::Int(1)]
|
|
}
|
|
|
|
fn calc(program: &str, range: Vec<PdfObj>) -> PdfFunction {
|
|
let (d, data) = calculator(program, range);
|
|
PdfFunction::parse(&d, Some(&data), &mut |_: &PdfObj| None).expect("parse type4")
|
|
}
|
|
|
|
#[test]
|
|
fn type4_arithmetic_and_stack_operators() {
|
|
// x/2 + x^2
|
|
let f = calc("{ dup 0.5 mul exch 2 exp add }", unit_range());
|
|
assert!((f.eval(&[0.5]).expect("e")[0] - 0.5).abs() < 1e-9);
|
|
assert_eq!(f.eval(&[1.0]).expect("e"), vec![1.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_conditionals_branch() {
|
|
let f = calc("{ 0.5 lt { 0 } { 1 } ifelse }", unit_range());
|
|
assert_eq!(f.eval(&[0.25]).expect("e"), vec![0.0]);
|
|
assert_eq!(f.eval(&[0.75]).expect("e"), vec![1.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_multiple_outputs_come_off_in_order() {
|
|
let range = vec![
|
|
PdfObj::Int(0), PdfObj::Int(1),
|
|
PdfObj::Int(0), PdfObj::Int(1),
|
|
PdfObj::Int(0), PdfObj::Int(1),
|
|
];
|
|
let f = calc("{ dup dup 0.5 mul }", range);
|
|
let out = f.eval(&[0.8]).expect("e");
|
|
assert!((out[0] - 0.8).abs() < 1e-9 && (out[1] - 0.8).abs() < 1e-9 && (out[2] - 0.4).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_roll_index_and_degree_trigonometry() {
|
|
// 1 2 3 with "3 1 roll" becomes 3 1 2; two pops leave the 3.
|
|
let f = calc(
|
|
"{ pop 1 2 3 3 1 roll pop pop }",
|
|
vec![PdfObj::Int(0), PdfObj::Int(10)],
|
|
);
|
|
assert_eq!(f.eval(&[0.0]).expect("e"), vec![3.0]);
|
|
let sine = calc("{ pop 90 sin }", unit_range());
|
|
assert!((sine.eval(&[0.0]).expect("e")[0] - 1.0).abs() < 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_atan_program_renders_polar_space() {
|
|
let program = "{ pop exch 360 mul dup sin exch cos atan 360 div sub abs \
|
|
.1 exch sub dup 0 lt { pop 0 } if 10 mul sqrt 1 exch sub dup dup }";
|
|
let range: Vec<PdfObj> = (0..3)
|
|
.flat_map(|_| [PdfObj::Int(0), PdfObj::Int(1)])
|
|
.collect();
|
|
let f = calc(program, range);
|
|
let close = |got: Vec<f64>, want: f64| {
|
|
for g in got {
|
|
assert!((g - want).abs() < 1e-9, "got {g}");
|
|
}
|
|
};
|
|
close(f.eval(&[10.0 / 255.0, 250.0 / 255.0, 0.0]).expect("e"), 1.0);
|
|
close(f.eval(&[128.0 / 255.0, 128.0 / 255.0, 0.0]).expect("e"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_outputs_clamp_to_range() {
|
|
let f = calc("{ 4 mul }", unit_range());
|
|
assert_eq!(f.eval(&[0.9]).expect("e"), vec![1.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_broken_programs_fall_back_to_the_range_floor() {
|
|
let f = calc("{ pop frobnicate }", unit_range());
|
|
assert_eq!(f.eval_or_range_floor(&[0.5]), vec![0.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn type4_comments_and_nested_procedures_parse() {
|
|
let f = calc("{ % a comment\n true { 0.5 } if }", unit_range());
|
|
assert_eq!(f.eval(&[0.1]).expect("e"), vec![0.5]);
|
|
}
|