nigig-org/crates/apps/pdf/pdf-graphics/tests/codec_port.rs
andodeki ac8f8aa002
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
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
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
p2p-intel / engine (push) Has been cancelled
p2p-intel / notifications (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
p2p-intel / exchange-tab (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
chore: sync full working tree to gitdab
Whole-tree sync: cad-core/cad-ui split sources, nigig-build
construction_frame migration, pdf port progress, mpesa/pay/uikit/doc
updates, workspace members/profiles/lock, CI workflows and reviews.
See individual file history for details.
2026-09-12 07:15:24 +03:00

170 lines
6.2 KiB
Rust

//! 1:1 port of `dart-pdf/packages/pdf_graphics/test/render_command_codec_test.dart`
//! (tranche 1: synthetic round-trip, overprint tag, corpus round-trip, and
//! the f32 path precision contract).
//!
//! Codec fidelity is compared at the command level (`format_commands`):
//! a buffer that survives the wire byte-identical replays identically.
//! Replay itself is covered by `render_command_port.rs`.
//!
//! Not ported: worker state-scope compaction, image offload/placeholders
//! (nigig's wire always carries image bytes — no decline model), glyph
//! outline deduplication, decode regions, resolution caps, page budgets,
//! and raster-pixel helpers. Those are the background-worker render
//! pipeline, which nigig does not have.
use nigig_pdf_document::PdfDocument;
use nigig_pdf_graphics::content::parse_content_stream;
use nigig_pdf_graphics::path::PdfPathBuilder;
use nigig_pdf_graphics::recording::{format_commands, RecordingDevice, RenderCommand};
use nigig_pdf_graphics::wire::{decode, encode};
fn roundtrip(cmds: &[RenderCommand]) -> Vec<RenderCommand> {
let bytes = encode(cmds);
decode(&bytes).expect("decodes")
}
fn record(content: &str) -> Vec<RenderCommand> {
let ops = parse_content_stream(content.as_bytes()).expect("content parses");
RecordingDevice::from_ops(&ops)
}
#[test]
fn synthetic_buffers_round_trip() {
let cases = [
"q 2 0 0 2 10 10 cm 0 0 1 rg 5 5 20 30 re f 1 0 0 RG 4 w 0 0 10 10 re S Q",
"[3 2] 1.5 d 1 w 10 10 m 90 90 l S",
"0 0 5 5 re W n 0 0 1 rg 0 0 10 10 re f",
"BT /F1 24 Tf 72 720 Td (Hello, world!) Tj ET",
// Word spacing (Tw) with char spacing exercises kern-adjusted runs.
"BT /F1 10 Tf 5 Tw 0.2 Tc 72 700 Td ( ab ) Tj ET",
"q q 0 0 1 1 re f Q q 1 1 2 2 re f Q Q",
"10 10 m 20 30 40 30 50 10 c f",
"0 0 10 10 re 2 2 6 6 re f*",
];
for content in cases {
let original = record(content);
assert!(!original.is_empty(), "fixture should paint something");
let restored = roundtrip(&original);
assert_eq!(
format_commands(&restored),
format_commands(&original),
"round-trip diverged for {content:?}"
);
}
}
#[test]
fn byte_output_is_stable_across_two_serializations() {
let original =
record("q 0 0 1 rg 5 5 20 30 re f Q BT /F1 12 Tf 10 10 Td (hi) Tj ET");
let a = encode(&original);
let b = encode(&original);
assert_eq!(a, b);
}
#[test]
fn overprint_state_round_trips_fill_stroke_mode() {
let commands = vec![
RenderCommand::SetOverprint { fill: true, stroke: false, mode: 1 },
RenderCommand::SetOverprint { fill: false, stroke: true, mode: 0 },
];
let restored = roundtrip(&commands);
assert_eq!(restored.len(), 2);
assert!(
matches!(
&restored[0],
RenderCommand::SetOverprint { fill: true, stroke: false, mode: 1 }
),
"got {:?}",
restored[0]
);
assert!(
matches!(
&restored[1],
RenderCommand::SetOverprint { fill: false, stroke: true, mode: 0 }
),
"got {:?}",
restored[1]
);
}
#[test]
fn path_coordinates_survive_at_f32_precision() {
// Geometry rides the wire as f32 (dart's Float32List exactness): the
// codec promises the f32 image, not the f64 original.
let mut b = PdfPathBuilder::new();
b.move_to(0.1, 0.2);
b.line_to(0.3, 1.0 / 3.0);
let path = b.take_path();
let restored = roundtrip(&[RenderCommand::Path(path)]);
assert_eq!(restored.len(), 1);
let RenderCommand::Path(back) = &restored[0] else {
panic!("expected a Path");
};
// Decoded coords equal the f32 truncation of the inputs, exactly.
let coords: Vec<f64> = back
.segments()
.iter()
.flat_map(|s| match *s {
nigig_pdf_graphics::path::PdfPathSegment::MoveTo { x, y }
| nigig_pdf_graphics::path::PdfPathSegment::LineTo { x, y } => vec![x, y],
nigig_pdf_graphics::path::PdfPathSegment::CubicTo { x1, y1, x2, y2, x3, y3 } => {
vec![x1, y1, x2, y2, x3, y3]
}
nigig_pdf_graphics::path::PdfPathSegment::Close => Vec::new(),
})
.collect();
for (got, want) in coords.iter().zip([0.1, 0.2, 0.3, 1.0 / 3.0]) {
assert_eq!(*got as f32, want as f32, "f32 image must match");
assert!(
(*got - want).abs() < 1e-6,
"f32 rounding only, got {got} want {want}"
);
}
}
// Real pages exercise the fragile callbacks. Nigig's wire always carries
// image bytes (no decline/placeholder model — a documented deviation from
// dart's cos-gated serialization), so every parseable page must round-trip.
const GHENT: &str = "/Users/aok/Projects/rustdev/CratesCode/dart-pdf/test_corpora/ghent/1-CMYK";
#[test]
fn corpus_buffers_round_trip() {
let files = [
"GWG168_Softmasks_Vector_part1_X4.pdf",
"GWG1610_Softmasks_Text_part1_X4.pdf",
"GWG160_Transp_Basic_BM_DeviceCMYK_Non-knockout_X4.pdf",
"GWG161_Transp_Basic_BM_DeviceCMYK_Knockout_X4.pdf",
"GWG060_Shading_x1a.pdf",
"GWG061_Shading_x1a.pdf",
];
let mut ran = 0;
for name in files {
let path = format!("{GHENT}/{name}");
let Ok(bytes) = std::fs::read(&path) else {
eprintln!("SKIP (missing corpus file): {path}");
continue;
};
let Ok(mut doc) = PdfDocument::parse(&bytes) else {
eprintln!("SKIP (unparseable — likely compressed xref): {path}");
continue;
};
for i in 0..doc.page_count() {
let Ok(page) = doc.page(i) else { continue };
let Ok(ops) = parse_content_stream(&page.content_data) else { continue };
let original = RecordingDevice::from_ops(&ops);
if original.is_empty() {
continue;
}
let restored = roundtrip(&original);
ran += 1;
assert_eq!(
format_commands(&restored),
format_commands(&original),
"{name} page {i} diverged after round-trip"
);
}
}
eprintln!("corpus_buffers_round_trip compared {ran} pages");
assert!(ran > 0, "no corpus pages ran — checkout missing?");
}