//! 1:1 port of `dart-pdf/packages/pdf_graphics/test/overprint_test.dart` //! (state half: /OP /op /OPM parsing, q/Q survival, device delivery). //! //! The colorant-buffer corpus tests (GWG030 glyph resolution, GWG205 //! stroked replay) need the ink-space compositor delivery path and are //! pinned by `colorant_buffer_test.dart` territory, not here. use std::collections::HashMap; use nigig_pdf_cos::{PdfDict, PdfObj}; use nigig_pdf_graphics::content::parse_content_stream; use nigig_pdf_graphics::recording::{RecordingDevice, RenderCommand}; use nigig_pdf_graphics::transparency::ExtGState; fn gs(op: Option, op_lower: Option, opm: Option) -> PdfDict { let mut d = PdfDict::new(); d.set("Type", PdfObj::Name("ExtGState".to_string())); if let Some(v) = op { d.set("OP", PdfObj::Bool(v)); } if let Some(v) = op_lower { d.set("op", PdfObj::Bool(v)); } if let Some(v) = opm { d.set("OPM", PdfObj::Int(v)); } d } fn parse_gs(dict: &PdfDict) -> ExtGState { let mut resolve = |_: &PdfObj| -> Option<(PdfObj, Option>)> { None }; let mut lookup = |_: &str| -> Option { None }; let mut errors = Vec::new(); ExtGState::parse(dict, &mut resolve, &mut lookup, &mut errors) } /// Interpret `content` against ExtGState resources and return the overprint /// tuples delivered, in order — mirroring dart's `_run` helper. fn run(content: &str, entries: Vec<(&str, PdfDict)>) -> Vec<(bool, bool, u8)> { let ops = parse_content_stream(content.as_bytes()).expect("content parses"); let mut device = RecordingDevice::new(); for (name, dict) in entries { device.register_ext_gstate(name, parse_gs(&dict)); } nigig_pdf_graphics::content::interpret_ops(&ops, &mut device).expect("interpret"); device .commands .iter() .filter_map(|c| match c { RenderCommand::SetOverprint { fill, stroke, mode } => Some((*fill, *stroke, *mode)), _ => None, }) .collect() } #[test] fn op_sets_fill_op_stroke_opm_mode() { let ops = run( "/GS gs 0 0 100 100 re f", vec![("GS", gs(Some(true), Some(true), Some(1)))], ); assert_eq!(ops, vec![(true, true, 1)]); } #[test] fn op_alone_drives_fill_when_op_absent() { // Backward-compat rule (§8.6.7): with no /op key, /OP drives fill // overprint as well. let ops = run("/GS gs 0 0 100 100 re f", vec![("GS", gs(Some(true), None, None))]); assert_eq!(ops, vec![(true, true, 0)]); } #[test] fn explicit_op_overrides_the_op_fallback_for_fill() { let ops = run( "/GS gs 0 0 100 100 re f", vec![("GS", gs(Some(true), Some(false), None))], ); assert_eq!(ops, vec![(false, true, 0)]); } #[test] fn opm_is_clamped_to_zero_or_one() { let ops = run("/GS gs f", vec![("GS", gs(None, Some(true), Some(5)))]); assert_eq!(ops, vec![(true, false, 1)]); } #[test] fn unchanged_overprint_state_is_not_redelivered() { let ops = run( "/GS gs /GS gs f", vec![("GS", gs(Some(true), Some(true), None))], ); assert_eq!(ops, vec![(true, true, 0)], "second identical gs is a no-op"); } #[test] fn q_q_restores_overprint_state_and_renotifies() { // Turn overprint on, save, turn it off, then Q back to the on state. let ops = run( "/ON gs q /OFF gs Q f", vec![ ("ON", gs(Some(true), Some(true), Some(1))), ("OFF", gs(Some(false), Some(false), Some(0))), ], ); assert_eq!( ops, vec![(true, true, 1), (false, false, 0), (true, true, 1)], "Q restoring the on state must notify again" ); } // ---------- GWG030 gray/K/separation overprint fixture (state half) ---------- const SPOT: &str = "/Users/aok/Projects/rustdev/CratesCode/dart-pdf/test_corpora/ghent/2-SPOT"; fn page_ext_gstates( doc: &mut nigig_pdf_document::PdfDocument, page_index: usize, ) -> Option<(Vec, HashMap)> { let page_ref = doc.page_object_ref(page_index)?; let page_obj = doc.resolve_ref(page_ref).ok()?; let resources = doc.resolve(page_obj.as_dict()?.get("Resources")?).ok()?; let gs_dict = doc .resolve(resources.as_dict()?.get("ExtGState")?) .ok()?; let gs_dict = gs_dict.as_dict()?.clone(); let content = doc.page(page_index).ok()?.content_data.clone(); let mut out = HashMap::new(); for (name, obj) in gs_dict.map.iter() { let resolved = doc.resolve(obj).ok()?; let dict = resolved.as_dict()?.clone(); out.insert(name.clone(), parse_gs(&dict)); } Some((content, out)) } #[test] fn every_op_extgstate_reaches_the_device() { let path = format!("{SPOT}/GWG030_Gray_K_black_OP_X1.pdf"); let Ok(bytes) = std::fs::read(&path) else { eprintln!("SKIP (missing corpus file): {path}"); return; }; let mut doc = match nigig_pdf_document::PdfDocument::parse(&bytes) { Ok(d) => d, Err(_) => { eprintln!("SKIP (unparseable — likely compressed xref): {path}"); return; } }; let Some((content, states)) = page_ext_gstates(&mut doc, 0) else { panic!("GWG030 page 0 has no ExtGState resources"); }; assert!(!states.is_empty(), "fixture must carry ExtGStates"); let ops = parse_content_stream(&content).expect("content parses"); let mut device = RecordingDevice::new(); for (name, gs) in &states { device.register_ext_gstate(name, gs.clone()); } nigig_pdf_graphics::content::interpret_ops(&ops, &mut device).expect("interpret"); let overprints: Vec<(bool, bool, u8)> = device .commands .iter() .filter_map(|c| match c { RenderCommand::SetOverprint { fill, stroke, mode } => Some((*fill, *stroke, *mode)), _ => None, }) .collect(); assert!( !overprints.is_empty(), "overprint state never reached the device" ); assert!( overprints.iter().any(|(f, s, _)| *f || *s), "at least one patch enables fill or stroke overprint" ); assert!( overprints.iter().any(|(_, _, m)| *m == 1), "OPM 1 (nonzero overprint mode) must be delivered" ); } // NOTE (not ported): the GWG205 spatial-stroked replay test and the GWG030 // sub-cell glyph resolution test need the colorant-buffer `resolveOverprint` // delivery path (ink-space composite with the flag cleared). That machinery // lives in `colorant_buffer_test.dart` territory; nigig's `composite.rs` // has the per-colorant model but no interpreter delivery flag yet.