nigig-org/crates/apps/pdf/pdf-graphics/tests/reflow.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

108 lines
4.4 KiB
Rust

//! Phase 8 exit criterion: reflow integration tests.
//!
//! The reflow unit tests in `reflow.rs` build runs by hand. These start
//! from a real PDF — parse, interpret, extract, reflow — because a reflow
//! that works on hand-built runs but not on a real content stream is not
//! a reflow at all. The corpus is the same one the search and selection
//! exit tests use, so the whole "text intelligence" phase is exercised
//! against one shared set of pages.
//!
//! See `REVIEWS/adr/0034-pdf-text-search-and-layout.md`.
use std::path::PathBuf;
use nigig_pdf_document::PdfDocument;
use nigig_pdf_graphics::content::parse_content_stream;
use nigig_pdf_graphics::recording::RecordingDevice;
use nigig_pdf_graphics::reflow::{reflow_from_commands, ReflowOptions};
use nigig_pdf_graphics::text::PageText;
fn corpus(relative: &str) -> Vec<u8> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../tests/corpus")
.join(relative);
std::fs::read(&path).unwrap_or_else(|e| panic!("missing fixture {}: {e}", path.display()))
}
/// Parse a fixture, render it to commands, and reflow page 0.
fn reflowed_text(relative: &str) -> String {
let data = corpus(relative);
let mut doc = PdfDocument::parse(&data).expect("fixture parses");
let page = doc.page(0).expect("page 0");
let mut device = RecordingDevice::new();
for (name, resource) in &page.fonts {
device.register_font(
name,
nigig_pdf_graphics::font::GlyphWidths::from_standard_font(&resource.base_font),
);
}
let ops = parse_content_stream(&page.content_data).expect("content parses");
nigig_pdf_graphics::content::interpret_ops(&ops, &mut device).expect("interprets");
let commands = device.into_commands();
// The full-width-command pipeline, as the reader would call it.
let options = ReflowOptions::default();
let reflowed = reflow_from_commands(&commands, &options).expect("reflows");
// One paragraph per blank line, one line per newline.
reflowed.plain_text()
}
#[test]
fn a_real_page_reflows_to_plain_text() {
// `two_columns.pdf` is the layout torture test of this phase: it emits
// runs across and down, shares baselines between columns, and splits a
// word by kerning. A reflow must still produce readable text, left to
// right and top to bottom, wider than it is deep.
let text = reflowed_text("basic/two_columns.pdf");
assert!(
text.contains("Left one") && text.contains("Right one"),
"both columns present in reflow, got {text:?}"
);
}
#[test]
fn reflow_preserves_all_text_from_a_real_page() {
// Nothing may be lost in the reflow: the reflowed plain text and the
// direct extraction must contain the same tokens (order may differ,
// but the vocabulary may not shrink).
let options = ReflowOptions::default();
let data = corpus("basic/two_columns.pdf");
let mut doc = PdfDocument::parse(&data).unwrap();
let page = doc.page(0).unwrap();
let mut device = RecordingDevice::new();
for (name, resource) in &page.fonts {
device.register_font(
name,
nigig_pdf_graphics::font::GlyphWidths::from_standard_font(&resource.base_font),
);
}
let ops = parse_content_stream(&page.content_data).unwrap();
nigig_pdf_graphics::content::interpret_ops(&ops, &mut device).unwrap();
let commands = device.into_commands();
let extracted = PageText::from_commands(&commands).plain_text();
let reflowed = reflow_from_commands(&commands, &options).unwrap();
let reflow_text = reflowed.paragraphs.iter().map(|p| p.text.as_str()).collect::<Vec<_>>().join(" ");
// The vocabulary may change in *form* — a reflow reconstructs words that
// the writer split across runs by kerning ("Hy" + "phen" → "Hyphen") and
// re-inserts word spacing — but it may not *lose* any glyph. Compare the
// significant characters as a multiset, which is invariant under joining
// and splitting words.
let sig = |s: &str| -> Vec<char> {
let mut v: Vec<char> = s.chars().filter(|c| !c.is_whitespace()).collect();
v.sort_unstable();
v
};
let extracted_chars = sig(&extracted);
let reflow_chars = sig(&reflow_text);
for c in &extracted_chars {
assert!(
reflow_chars.contains(c),
"reflow lost glyph {c:?}; extracted {extracted:?}, reflowed {reflow_text:?}"
);
}
}