nigig-org/crates/apps/pdf/pdf-graphics/tests/shading_port.rs
andodeki e899a271c6
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
feat(pdf): shading port test (12 passed) + function updates
2026-09-12 09:22:48 +03:00

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]);
}