//! 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::Array(vals) } fn exponential(c0: Vec, c1: Vec) -> 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) -> (PdfDict, Vec) { 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, 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) -> (PdfDict, Vec) { 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 { vec![PdfObj::Int(0), PdfObj::Int(1)] } fn calc(program: &str, range: Vec) -> 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 = (0..3) .flat_map(|_| [PdfObj::Int(0), PdfObj::Int(1)]) .collect(); let f = calc(program, range); let close = |got: Vec, 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]); }