pdftract/tests/sdk-conformance/fixtures/generate_proper_fixtures.rs
jedarden d0f52751ce fix(pdftract-39gey): fix indent trigger to not split drop-cap paragraphs
The indent trigger was using .abs() which fired on both increased indent
(non-indented → indented) AND decreased indent (indented → non-indented).
This caused drop-cap style paragraphs (indented first line, flush-left
continuation) to incorrectly split into two blocks.

Per plan Phase 4.4 heuristic #2, indent change should only trigger when the
current line is MORE indented (to the right, larger x0) than the block
average - i.e., a new paragraph starting after non-indented text. It should
NOT trigger for decreased indent (first line indented, rest flush-left).

Fix: Remove .abs() and only check if line_x0 - block_avg_x0 > threshold.

Tests:
- test_indented_first_line_new_block: PASS (non-indented → indented splits)
- test_indented_first_line_of_paragraph_not_split: PASS (drop cap stays together)
- All 179 line module tests: PASS
2026-06-07 13:43:19 -04:00

467 lines
18 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.

#!/usr/bin/env rust-script
//! Generate proper PDF fixtures for SDK conformance testing.
//!
//! Creates valid PDF documents with the actual content expected by the conformance tests.
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
/// Escape special characters in PDF strings
fn escape_pdf_string(text: &str) -> String {
text.chars()
.flat_map(|c| match c {
'(' => vec!['\\', '('],
')' => vec!['\\', ')'],
'\\' => vec!['\\', '\\'],
'\n' => vec!['\\', 'n'],
'\r' => vec!['\\', 'r'],
'\t' => vec!['\\', 't'],
c => vec![c],
})
.collect()
}
/// Create a PDF with multi-line content at specified y positions
fn create_content_pdf(path: &Path, lines: &[(f32, &str)], title: &str) -> std::io::Result<()> {
let mut pdf = String::new();
let mut offsets: Vec<usize> = Vec::new();
// Header
pdf.push_str("%PDF-1.4\n");
// Object 1: Catalog
offsets.push(pdf.len());
pdf.push_str(&format!(
"1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Title ({})\n>>\nendobj\n",
escape_pdf_string(title)
));
// Build content stream with multiple lines
let mut content = String::new();
for (y, line) in lines {
content.push_str(&format!("BT\n/F1 12 Tf\n50 {} Td\n({}) Tj\nET\n", y, escape_pdf_string(line)));
}
// Object 2: Pages node
offsets.push(pdf.len());
pdf.push_str("2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1>>\nendobj\n");
// Object 3: Page
offsets.push(pdf.len());
pdf.push_str(&format!(
"3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n/Resources <<\n/Font <<\n/F1 5 0 R\n>>\n>>\n>>\nendobj\n"
));
// Object 4: Content stream
offsets.push(pdf.len());
pdf.push_str(&format!(
"4 0 obj\n<<\n/Length {}>>\nstream\n{}\nendstream\nendobj\n",
content.len(),
content
));
// Object 5: Font
offsets.push(pdf.len());
pdf.push_str("5 0 obj\n<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>\nendobj\n");
// Build xref table
let xref_start = pdf.len();
pdf.push_str("xref\n");
pdf.push_str(&format!("0 {}\n", offsets.len() + 1));
pdf.push_str("0000000000 65535 f \n");
for &offset in &offsets {
pdf.push_str(&format!("{:010} 00000 n \n", offset));
}
// Trailer
pdf.push_str(&format!(
"trailer\n<<\n/Size {}\n/Root 1 0 R\n>>\nstartxref\n{}\n%%EOF\n",
offsets.len() + 1,
xref_start
));
// Write to file
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, pdf.as_bytes())?;
Ok(())
}
/// Create a multi-page PDF
fn create_multi_page_pdf(path: &Path, pages: &[Vec<(f32, String)>], title: &str) -> std::io::Result<()> {
let mut pdf = String::new();
let mut offsets: Vec<usize> = Vec::new();
// Header
pdf.push_str("%PDF-1.4\n");
// Object 1: Catalog
offsets.push(pdf.len());
pdf.push_str(&format!(
"1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n/Title ({})\n>>\nendobj\n",
escape_pdf_string(title)
));
// Build page objects and content streams
let mut page_refs = Vec::new();
let font_obj_num = 3 + pages.len() * 2;
for (page_idx, page_lines) in pages.iter().enumerate() {
let content_obj = 3 + page_idx * 2;
let page_obj = 4 + page_idx * 2;
// Build content stream for this page
let mut content = String::new();
for (y, line) in page_lines {
content.push_str(&format!("BT\n/F1 12 Tf\n50 {} Td\n({}) Tj\nET\n", y, escape_pdf_string(line)));
}
// Content stream
offsets.push(pdf.len());
pdf.push_str(&format!(
"{} 0 obj\n<<\n/Length {}>>\nstream\n{}\nendstream\nendobj\n",
content_obj,
content.len(),
content
));
// Page object
offsets.push(pdf.len());
pdf.push_str(&format!(
"{} 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents {} 0 R\n/Resources <<\n/Font <<\n/F1 {} 0 R\n>>\n>>\n>>\nendobj\n",
page_obj,
content_obj,
font_obj_num
));
page_refs.push(format!("{} 0 R", page_obj));
}
// Object 2: Pages node
offsets.push(pdf.len());
pdf.push_str(&format!(
"2 0 obj\n<<\n/Type /Pages\n/Kids [{}]\n/Count {}>>\nendobj\n",
page_refs.join(" "),
pages.len()
));
// Font object (last)
offsets.push(pdf.len());
pdf.push_str(&format!(
"{} 0 obj\n<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>\nendobj\n",
font_obj_num
));
// Build xref table
let xref_start = pdf.len();
pdf.push_str("xref\n");
pdf.push_str(&format!("0 {}\n", offsets.len() + 1));
pdf.push_str("0000000000 65535 f \n");
for &offset in &offsets {
pdf.push_str(&format!("{:010} 00000 n \n", offset));
}
// Trailer
pdf.push_str(&format!(
"trailer\n<<\n/Size {}\n/Root 1 0 R\n>>\nstartxref\n{}\n%%EOF\n",
offsets.len() + 1,
xref_start
));
// Write to file
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, pdf.as_bytes())?;
Ok(())
}
/// Create a receipt JSON file
fn create_receipt_json(path: &Path, valid: bool) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
let content = if valid {
r#"{"fingerprint": "stub-valid", "signature": "valid-signature"}"#
} else {
r#"{"fingerprint": "stub-tampered", "signature": "invalid-signature"}"#
};
file.write_all(content.as_bytes())?;
Ok(())
}
fn main() -> std::io::Result<()> {
// Try to find the workspace root by looking for Cargo.toml
let mut fixture_path = Path::new("tests/sdk-conformance/fixtures");
// If running from within fixtures directory, adjust path
if std::env::current_dir().unwrap().ends_with("fixtures") {
fixture_path = Path::new(".");
} else if std::env::current_dir().unwrap().ends_with("sdk-conformance") {
fixture_path = Path::new("fixtures");
} else if std::env::current_dir().unwrap().ends_with("tests") {
fixture_path = Path::new("sdk-conformance/fixtures");
}
println!("Creating proper fixtures in: {:?}", fixture_path);
println!("Creating proper fixtures in: {:?}", fixture_path);
// Scientific paper fixtures with actual academic content
let scientific_papers: Vec<(usize, Vec<(f32, String)>)> = vec![
(1, vec![
(700.0, "Title: Analysis of Machine Learning Algorithms".to_string()),
(680.0, "Abstract".to_string()),
(660.0, "This paper presents a comprehensive analysis of various".to_string()),
(640.0, "machine learning algorithms and their performance.".to_string()),
(620.0, "Introduction".to_string()),
(600.0, "Machine learning has become an essential tool in modern".to_string()),
(580.0, "data science and artificial intelligence research.".to_string()),
]),
(2, vec![
(700.0, "Title: Unicode Character Analysis: α, β, γ, 中文, العربية".to_string()),
(680.0, "Abstract".to_string()),
(660.0, "This study examines Unicode character handling in text extraction.".to_string()),
(640.0, "Introduction".to_string()),
(620.0, "Unicode support is critical for international document processing.".to_string()),
]),
(3, vec![
(700.0, "Title: Mathematical Formula Recognition".to_string()),
(680.0, "Abstract".to_string()),
(660.0, "We propose a method for extracting and rendering mathematical".to_string()),
(640.0, "equations and formulas from scientific documents.".to_string()),
(620.0, "Introduction".to_string()),
(600.0, "Mathematical notation presents unique challenges in PDF parsing.".to_string()),
]),
(4, vec![
(700.0, "Title: Document Structure Analysis".to_string()),
(680.0, "# Introduction".to_string()),
(660.0, "This paper examines document structure.".to_string()),
(640.0, "## Methods".to_string()),
(620.0, "We employ novel techniques.".to_string()),
(600.0, "### Results".to_string()),
(580.0, "Our approach shows significant improvements.".to_string()),
]),
(5, vec![
(700.0, "# Header Content".to_string()),
(660.0, "Main document body content.".to_string()),
(620.0, "## Subsection".to_string()),
(580.0, "Additional content here.".to_string()),
(540.0, "# Footer Information".to_string()),
]),
(6, vec![
(700.0, "Title: Streaming Document Processing".to_string()),
(680.0, "Schema Version: 1.0".to_string()),
(660.0, "Total Pages: 5".to_string()),
(640.0, "Page 1 content begins here.".to_string()),
]),
(7, vec![
(700.0, "Abstract".to_string()),
(660.0, "This is the abstract of the paper.".to_string()),
(620.0, "Introduction".to_string()),
(580.0, "This is the introduction section.".to_string()),
]),
(8, vec![
(700.0, "Title: Year Analysis".to_string()),
(660.0, "Published in 2024, with references to 2023 and 2025.".to_string()),
(620.0, "The study spanned from 2020 to 2024.".to_string()),
]),
(9, vec![
(700.0, "Title: Test Document".to_string()),
(660.0, "This document contains no special patterns.".to_string()),
]),
(10, vec![
(700.0, "Title: Author Analysis".to_string()),
(680.0, "Author: Jane Doe".to_string()),
(660.0, "Creator: Research Institute".to_string()),
(640.0, "Abstract: A study of document metadata.".to_string()),
]),
(11, vec![
(700.0, "Title: Hash Test Document".to_string()),
(660.0, "This document is used for hash testing.".to_string()),
]),
(12, vec![
(700.0, "Title: Content Stability Test".to_string()),
(660.0, "Verifies that extraction produces consistent results.".to_string()),
]),
(13, vec![
(700.0, "Title: Academic Paper Classification".to_string()),
(680.0, "Abstract".to_string()),
(660.0, "This paper presents novel research on classification.".to_string()),
(640.0, "Introduction".to_string()),
(620.0, "Our method uses advanced machine learning techniques.".to_string()),
(600.0, "Methods".to_string()),
(580.0, "We evaluated on multiple datasets.".to_string()),
(560.0, "Results".to_string()),
(540.0, "The proposed approach achieves 95% accuracy.".to_string()),
(520.0, "References".to_string()),
]),
(14, vec![
(700.0, "Title: Scientific Paper Classification".to_string()),
(680.0, "Abstract".to_string()),
(660.0, "This paper examines scientific document analysis.".to_string()),
(640.0, "Introduction".to_string()),
(620.0, "Scientific papers have distinctive structure.".to_string()),
(600.0, "Methods".to_string()),
(580.0, "We developed a new classification method.".to_string()),
(560.0, "Results".to_string()),
(540.0, "Results show strong performance.".to_string()),
(520.0, "References".to_string()),
]),
];
for (i, content) in scientific_papers {
let path = fixture_path.join(format!("scientific_paper/{:02}.pdf", i));
let content_refs: Vec<(f32, &str)> = content.iter().map(|(y, s)| (*y, s.as_str())).collect();
create_content_pdf(&path, &content_refs, &format!("Scientific Paper {}", i))?;
println!("Created scientific_paper/{:02}.pdf", i);
}
// Misc fixtures
for i in 1..=3 {
let content = vec![
(700.0, format!("Misc Document {}", i)),
(660.0, "Some sample content here.".to_string()),
];
let content_refs: Vec<(f32, &str)> = content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join(format!("misc/{:02}.pdf", i));
create_content_pdf(&path, &content_refs, &format!("Misc {}", i))?;
println!("Created misc/{:02}.pdf", i);
}
// Invoice fixtures
let invoice_content = vec![
(700.0, "INVOICE".to_string()),
(660.0, "Invoice Number: 12345".to_string()),
(620.0, "Total Amount: $100.00".to_string()),
];
let invoice_refs: Vec<(f32, &str)> = invoice_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("invoice/01.pdf");
create_content_pdf(&path, &invoice_refs, "Invoice")?;
println!("Created invoice/01.pdf");
// Contract fixtures
let contract_content = vec![
(700.0, "SERVICE AGREEMENT".to_string()),
(660.0, "This Agreement is made between parties.".to_string()),
(620.0, "| Term | Description |".to_string()),
(580.0, "|-------|-------------|".to_string()),
(540.0, "| Price | $500 |".to_string()),
];
let contract_refs: Vec<(f32, &str)> = contract_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("contract/01.pdf");
create_content_pdf(&path, &contract_refs, "Contract")?;
println!("Created contract/01.pdf");
// Encrypted PDF (simulated - the PDF itself isn't actually encrypted)
let encrypted_content = vec![
(700.0, "Encrypted Document".to_string()),
(660.0, "This document requires password protection.".to_string()),
];
let encrypted_refs: Vec<(f32, &str)> = encrypted_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("encrypted/encrypted.pdf");
create_content_pdf(&path, &encrypted_refs, "Encrypted PDF")?;
println!("Created encrypted/encrypted.pdf");
// Fillable form
let form_content = vec![
(700.0, "Application Form".to_string()),
(660.0, "Name: ______________".to_string()),
(620.0, "Email: ______________".to_string()),
];
let form_refs: Vec<(f32, &str)> = form_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("fillable-form/form.pdf");
create_content_pdf(&path, &form_refs, "Fillable Form")?;
println!("Created fillable-form/form.pdf");
// Mixed content (multi-page)
let mixed_pages: Vec<Vec<(f32, String)>> = vec![
vec![(700.0, "Page 1: Vector Content".to_string()), (660.0, "This is vector text.".to_string())],
vec![(700.0, "Page 2: Mixed Content".to_string()), (660.0, "Simulated scanned content.".to_string())],
];
let path = fixture_path.join("mixed/mixed.pdf");
create_multi_page_pdf(&path, &mixed_pages, "Mixed Content Document")?;
println!("Created mixed/mixed.pdf");
// Large documents
for pages in [50, 100] {
let large_pages: Vec<Vec<(f32, String)>> = (0..pages)
.map(|i| vec![(700.0, format!("Page {}: Document Content", i + 1))])
.collect();
let path = fixture_path.join(format!("large/{}pages.pdf", pages));
create_multi_page_pdf(&path, &large_pages, &format!("{} Page Document", pages))?;
println!("Created large/{}pages.pdf", pages);
}
// Vertical writing
let vertical_content = vec![
(700.0, "Vertical Text".to_string()),
(660.0, "Japanese text 日本語".to_string()),
];
let vertical_refs: Vec<(f32, &str)> = vertical_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("vertical/vertical.pdf");
create_content_pdf(&path, &vertical_refs, "Vertical Text Document")?;
println!("Created vertical/vertical.pdf");
// Code
let code_content = vec![
(700.0, "Code Example".to_string()),
(660.0, "```".to_string()),
(620.0, "function test() {".to_string()),
(580.0, " return true;".to_string()),
(540.0, "}".to_string()),
(500.0, "```".to_string()),
];
let code_refs: Vec<(f32, &str)> = code_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("code/code.pdf");
create_content_pdf(&path, &code_refs, "Code Sample")?;
println!("Created code/code.pdf");
// XMP metadata
let xmp_content = vec![
(700.0, "XMP Metadata Document".to_string()),
(660.0, "This document contains XMP metadata.".to_string()),
];
let xmp_refs: Vec<(f32, &str)> = xmp_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
let path = fixture_path.join("xmp/xmp-metadata.pdf");
create_content_pdf(&path, &xmp_refs, "XMP Metadata Document")?;
println!("Created xmp/xmp-metadata.pdf");
// Receipts
create_receipt_json(&fixture_path.join("receipts/valid-receipt.receipt.json"), true)?;
create_receipt_json(&fixture_path.join("receipts/tampered-receipt.receipt.json"), false)?;
let receipt_content = vec![
(700.0, "RECEIPT".to_string()),
(660.0, "Store: Example Store".to_string()),
(620.0, "Total: $50.00".to_string()),
];
let receipt_refs: Vec<(f32, &str)> = receipt_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
create_content_pdf(&fixture_path.join("receipts/valid-receipt.pdf"), &receipt_refs, "Valid Receipt")?;
let tampered_content = vec![
(700.0, "RECEIPT".to_string()),
(660.0, "Store: Different Store".to_string()),
(620.0, "Total: $999.00".to_string()),
];
let tampered_refs: Vec<(f32, &str)> = tampered_content.iter().map(|(y, s)| (*y, s.as_str())).collect();
create_content_pdf(&fixture_path.join("receipts/tampered-receipt.pdf"), &tampered_refs, "Tampered Receipt")?;
println!("Created receipt fixtures");
// Broken/corrupt PDF
let path = fixture_path.join("broken/corrupt.pdf");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(&path)?;
file.write_all(b"%PDF-1.4\nThis is intentionally broken\n%%EOF")?;
println!("Created broken/corrupt.pdf");
println!("\nAll proper fixtures created successfully!");
Ok(())
}