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.
108 lines
4.4 KiB
Rust
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:?}"
|
|
);
|
|
}
|
|
}
|