nigig-org/crates/apps/pdf/pdf-graphics/tests/overprint_port.rs
andodeki b1045d79d4
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
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): graphics/document port progress + android gates
- pdf-graphics: new cff, truetype, cjk_cmap, disk_cache, document_ai,
  path, reflow, text_diff modules + port tests (all green); syntax/type
  fixes for the aarch64 build (stray brace, &u8 derefs, f2dot14 assign).
- pdf-document: ocr, pdf_a, xmp modules + roundtrip tests; annotation
  editor updates.
- pdf-makepad: handle RenderCommand::SetOverprint (report via
  TransparencyError per ADR 0009, no silent drop); renderer/page_view
  updates for the new recording ops.
- matrix_client: nimanyatta native module (ungated; the aarch64 wrapper
  needs it unconditionally).
- pageflipnav home_screen: project_store sync is host-only until
  cad-ui's Widget Script derive is fixed for aarch64.

Verified: cargo check on all four crates clean at fork 66cc4f15f;
nigig-pdf-graphics tests pass; pageflipnav aarch64 release links.
2026-09-11 05:09:11 +03:00

189 lines
6.5 KiB
Rust

//! 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<bool>, op_lower: Option<bool>, opm: Option<i64>) -> 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<Vec<u8>>)> { None };
let mut lookup = |_: &str| -> Option<PdfObj> { 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<u8>, HashMap<String, ExtGState>)> {
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.