//! 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 { let bytes = encode(cmds); decode(&bytes).expect("decodes") } fn record(content: &str) -> Vec { 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 = 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?"); }