pdftract/gen_unmapped_comprehensive.rs
jedarden 7788719e0a docs(bf-ad2pp): analyze unmapped glyph fixture requirements
- Read design documents notes/bf-68f9i-design.md and notes/bf-68f9i-glyphs.md
- Identified 7 unmapped glyphs across 4 failure categories
- Identified 3 mapped AGL glyphs for comparison
- Verified existing generator implementation (gen_unmapped_comprehensive.rs)
- Confirmed fixture files exist and are correctly structured
- Documented implementation analysis in notes/bf-ad2pp.md

Closes bf-ad2pp
2026-07-03 19:17:02 -04:00

167 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Generate unmapped glyph test fixture for comprehensive Unicode recovery testing.
//!
//! Run with: cargo run --bin gen_unmapped_comprehensive
//!
//! This creates the unmapped-comprehensive.pdf fixture as specified in
//! notes/bf-68f9i-design.md, testing all 4 failure modes of the Unicode
//! recovery cascade plus success path verification.
use anyhow::{Context, Result};
use lopdf::dictionary;
use lopdf::{Dictionary, Object, Document};
use std::fs::File;
use std::io::Write;
fn create_simple_page_with_font(
content: &[u8],
font_dict: Dictionary,
doc: &mut Document,
) -> lopdf::ObjectId {
let mut page_dict = Dictionary::new();
page_dict.set("Type", "Page");
page_dict.set("MediaBox", Object::Array(vec![
Object::Real(0.0), Object::Real(0.0),
Object::Real(612.0), Object::Real(792.0)
]));
page_dict.set("Resources", dictionary! {
"Font" => dictionary! {
"F1" => font_dict
}
});
let content_stream_id = doc.new_object_id();
doc.objects.insert(content_stream_id, Object::Stream(lopdf::Stream::new(
dictionary! {},
content.to_vec()
)));
page_dict.set("Contents", Object::Reference(content_stream_id));
doc.add_object(page_dict)
}
/// Create the unmapped-comprehensive.pdf fixture as specified in notes/bf-68f9i-design.md.
///
/// This fixture includes:
/// - 6 unmapped glyphs (testing failure path): g001, g002, g003, CustomA, CustomB, NotAGlyph, glyph_0041
/// - 3 mapped glyphs (testing success path): A, B, space
/// - Single-page PDF with simple text layout
/// - Type1 font with custom encoding (no embedding)
fn create_unmapped_comprehensive_pdf() -> Result<()> {
let mut doc = Document::with_version("1.4");
// Font dictionary with custom /Differences encoding
// Codes 0-9 map to specific glyph names per the design spec
let font_dict = dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "UnmappedTestFont",
"Encoding" => dictionary! {
"Type" => "Encoding",
"Differences" => Object::Array(vec![
Object::Integer(0), // Start at code 0
// Unmapped glyphs (7 total) - these will NOT resolve through Level 2 AGL
Object::Name(b"g001".to_vec()), // code 0 -> PUA glyph
Object::Name(b"g002".to_vec()), // code 1 -> PUA glyph
Object::Name(b"g003".to_vec()), // code 2 -> PUA glyph
Object::Name(b"CustomA".to_vec()), // code 3 -> custom encoding
Object::Name(b"CustomB".to_vec()), // code 4 -> custom encoding
Object::Name(b"NotAGlyph".to_vec()), // code 5 -> orphaned glyph
Object::Name(b"glyph_0041".to_vec()), // code 6 -> non-AGL algorithmic
// Mapped glyphs (3 total) - standard AGL entries
Object::Name(b"A".to_vec()), // code 7 -> standard AGL
Object::Name(b"B".to_vec()), // code 8 -> standard AGL
Object::Name(b"space".to_vec()), // code 9 -> standard AGL
])
}
};
// Content stream with 3 lines as per design spec
// Line 1 (y=700): Three PUA glyphs (codes 0, 1, 2)
// Line 2 (y=680): Custom and orphaned glyphs (codes 3, 4, 5, 6)
// Line 3 (y=660): Mapped AGL glyphs (codes 7, 8, 9)
let content = b"BT
/F1 12 Tf
50 700 Td
<000102> Tj
50 680 Td
<03040506> Tj
50 660 Td
<070809> Tj
ET";
let page_id = create_simple_page_with_font(content, font_dict, &mut doc);
// Create pages dict
let mut pages_dict = Dictionary::new();
pages_dict.set("Type", "Pages");
pages_dict.set("Count", Object::Integer(1));
pages_dict.set("Kids", Object::Array(vec![Object::Reference(page_id)]));
let pages_id = doc.add_object(pages_dict);
// Update page parent
if let Some(Object::Dictionary(ref mut page_dict)) = doc.objects.get_mut(&page_id) {
page_dict.set("Parent", Object::Reference(pages_id));
}
// Create catalog
let mut catalog_dict = Dictionary::new();
catalog_dict.set("Type", "Catalog");
catalog_dict.set("Pages", Object::Reference(pages_id));
let catalog_id = doc.add_object(catalog_dict);
doc.trailer.set("Root", Object::Reference(catalog_id));
// Save PDF
let pdf_path = "tests/fixtures/encoding/unmapped-comprehensive.pdf";
doc.save(pdf_path)
.with_context(|| format!("Failed to create PDF: {}", pdf_path))?;
println!("Created: {}", pdf_path);
// Create ground truth .txt file
// Expected: 7 × U+FFFD + "AB " (for mapped glyphs A, B, space)
let txt_path = "tests/fixtures/encoding/unmapped-comprehensive.txt";
let mut txt_file = File::create(txt_path)
.with_context(|| format!("Failed to create ground truth: {}", txt_path))?;
// Line 1: 3 U+FFFD for g001, g002, g003
for _ in 0..3 {
write!(txt_file, "\u{FFFD}")?;
}
writeln!(txt_file)?;
// Line 2: 4 U+FFFD for CustomA, CustomB, NotAGlyph, glyph_0041
for _ in 0..4 {
write!(txt_file, "\u{FFFD}")?;
}
writeln!(txt_file)?;
// Line 3: "AB " for A, B, space
writeln!(txt_file, "AB ")?;
println!("Created: {} (7 × U+FFFD + \"AB \")", txt_path);
Ok(())
}
fn main() -> Result<()> {
println!("Generating unmapped glyph test fixture...");
println!("{}", "=".repeat(70));
// Ensure output directory exists
std::fs::create_dir_all("tests/fixtures/encoding")
.context("Failed to create fixtures directory")?;
println!("\n[1/1] Creating unmapped-comprehensive.pdf...");
println!("- 6 unmapped glyphs: g001, g002, g003, CustomA, CustomB, NotAGlyph, glyph_0041");
println!("- 3 mapped glyphs: A, B, space");
println!("- 3-line layout with custom Type1 encoding");
create_unmapped_comprehensive_pdf()?;
println!("\n{}", "=".repeat(70));
println!("Unmapped glyph fixture generated successfully!");
println!("\nFixtures created:");
println!(" tests/fixtures/encoding/unmapped-comprehensive.pdf");
println!(" tests/fixtures/encoding/unmapped-comprehensive.txt");
Ok(())
}