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
- 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.
189 lines
6.5 KiB
Rust
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.
|