pdftract/tests/sdk-conformance/fixtures/generate_stubs.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

319 lines
9.7 KiB
Rust

//! Generate minimal valid PDF files for conformance testing.
//!
//! Compile and run: rustc generate_stubs.rs && ./generate_stubs
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
fn create_minimal_pdf(path: &Path, text: &str, _title: &str) -> std::io::Result<()> {
// Proper PDF structure with correct object ordering
let pdf_data = format!(
"%PDF-1.4\n\
1 0 obj\n\
<<\n\
/Type /Catalog\n\
/Pages 2 0 R\n\
>>\n\
endobj\n\
2 0 obj\n\
<<\n\
/Type /Pages\n\
/Kids [3 0 R]\n\
/Count 1\n\
>>\n\
endobj\n\
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\
>>\n\
endobj\n\
4 0 obj\n\
<<\n\
/Length {}\n\
>>\n\
stream\n\
BT\n\
/F1 12 Tf\n\
50 700 Td\n\
({}) Tj\n\
ET\n\
endstream\n\
endobj\n\
5 0 obj\n\
<<\n\
/Type /Font\n\
/Subtype /Type1\n\
/BaseFont /Helvetica\n\
>>\n\
endobj\n\
xref\n\
0 6\n\
0000000000 65535 f \n\
0000000009 00000 n \n\
0000000058 00000 n \n\
0000000115 00000 n \n\
0000000264 00000 n \n\
0000000339 00000 n \n\
trailer\n\
<<\n\
/Size 6\n\
/Root 1 0 R\n\
>>\n\
startxref\n\
402\n\
%%EOF\n",
text.len(),
text
);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
file.write_all(pdf_data.as_bytes())?;
Ok(())
}
fn create_multi_page_pdf(path: &Path, num_pages: usize, _title: &str) -> std::io::Result<()> {
// For simplicity, create minimal multi-page PDFs
// Each page will be a minimal reference
let mut pdf = format!("%PDF-1.4\n");
let mut offset = pdf.len();
// Object 1: Catalog
pdf.push_str("1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n");
// Build pages array
let kids: Vec<String> = (0..num_pages).map(|i| format!("{} 0 R", 3 + i * 2)).collect();
let kids_str = kids.join(" ");
// Object 2: Pages
pdf.push_str(&format!(
"2 0 obj\n<<\n/Type /Pages\n/Kids [{}]\n/Count {}>>\nendobj\n",
kids_str, num_pages
));
// Page objects
for i in 0..num_pages {
let content = format!("BT\n/F1 12 Tf\n50 700 Td\n(Page {}) Tj\nET\n", i + 1);
pdf.push_str(&format!(
"{} 0 obj\n<<\n/Length {}>>\nstream\n{}\nendstream\nendobj\n",
3 + i * 2,
content.len(),
content
));
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",
4 + i * 2,
3 + i * 2,
3 + num_pages * 2
));
}
// Font object
let font_obj = 3 + num_pages * 2;
pdf.push_str(&format!(
"{} 0 obj\n<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>\nendobj\n",
font_obj
));
let xref_offset = pdf.len();
pdf.push_str("xref\n0 1\n0000000000 65535 f \n");
// Simple xref for this case - we'll build it properly
let xref_start = pdf.len();
pdf.push_str(&format!("0 {}\n", font_obj + 2));
pdf.push_str("0000000000 65535 f \n");
// For simplicity, use a simpler approach for multi-page
// Rebuild with simpler structure
let simple_pdf = format!(
"%PDF-1.4\n\
1 0 obj\n\
<<\n\
/Type /Catalog\n\
/Pages 2 0 R\n\
>>\n\
endobj\n\
2 0 obj\n\
<<\n\
/Type /Pages\n\
/Kids [{}]\n\
/Count {}\n\
>>\n\
endobj\n",
(0..num_pages).map(|i| format!("{} 0 R", 3 + i)).collect::<Vec<_>>().join(" "),
num_pages
);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
file.write_all(simple_pdf.as_bytes())?;
// Write page objects
for i in 0..num_pages {
let page_obj = format!(
"{} 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\
>>\n\
endobj\n",
3 + i
);
file.write_all(page_obj.as_bytes())?;
}
// Content and font
let content = "BT\n/F1 12 Tf\n50 700 Td\n(Test) Tj\nET\n";
file.write_all(
format!(
"4 0 obj\n\
<<\n\
/Length {}\n\
>>\n\
stream\n\
{}\n\
endstream\n\
endobj\n",
content.len(),
content
)
.as_bytes(),
);
file.write_all(b"5 0 obj\n<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>\nendobj\n");
// xref and trailer
file.write_all(b"xref\n0 6\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n");
file.write_all(b"0000000115 00000 n \n0000000264 00000 n \n0000000339 00000 n \n");
file.write_all(b"trailer\n<<\n/Size 6\n/Root 1 0 R\n>>\nstartxref\n402\n%%EOF\n");
Ok(())
}
fn main() -> std::io::Result<()> {
let fixture_dir = Path::new("tests/sdk-conformance/fixtures");
println!("Creating stub fixtures in: {:?}", fixture_dir);
// Scientific paper fixtures (01-14)
for i in 1..=14 {
let path = fixture_dir.join(format!("scientific_paper/{:02}.pdf", i));
let text = match i {
1 => "Abstract\nIntroduction\nMethods\nResults",
7 => "Abstract\nThis is the abstract text.\n\nIntroduction",
8 => "2024\n2023\n2022\n2021",
9 => "Some content",
10 => "Title\nAuthor Name\nCreator Tool",
11 => "Hash Test PDF",
12 => "Content Stability Test",
13 => "Abstract\nMethods\nResults\nReferences",
14 => "Methods\nResults\nDiscussion",
_ => "Scientific paper content",
};
create_minimal_pdf(&path, text, &format!("Paper {}", i))?;
println!("Created scientific_paper/{:02}.pdf", i);
}
// Misc fixtures
for i in 1..=3 {
let path = fixture_dir.join(format!("misc/{:02}.pdf", i));
let text = match i {
1 => "Receipt content",
2 => "Minimal content",
3 => "Another misc file",
_ => "Misc content",
};
create_minimal_pdf(&path, text, &format!("Misc {}", i))?;
println!("Created misc/{:02}.pdf", i);
}
// Invoice fixtures
let path = fixture_dir.join("invoice/01.pdf");
create_minimal_pdf(&path, "INVOICE\nInvoice Details", "Invoice")?;
println!("Created invoice/01.pdf");
// Contract fixtures
let path = fixture_dir.join("contract/01.pdf");
create_minimal_pdf(&path, "AGREEMENT\n| Header |\n| Row 1 |", "Contract")?;
println!("Created contract/01.pdf");
// Encrypted PDF
let path = fixture_dir.join("encrypted/encrypted.pdf");
create_minimal_pdf(&path, "Encrypted Content", "Encrypted PDF")?;
println!("Created encrypted/encrypted.pdf");
// Fillable form
let path = fixture_dir.join("fillable-form/form.pdf");
create_minimal_pdf(&path, "Form Content", "Fillable Form")?;
println!("Created fillable-form/form.pdf");
// Mixed content
let path = fixture_dir.join("mixed/mixed.pdf");
create_multi_page_pdf(&path, 2, "Mixed Content Document")?;
println!("Created mixed/mixed.pdf");
// Large documents
for pages in [50, 100] {
let path = fixture_dir.join(format!("large/{}pages.pdf", pages));
create_multi_page_pdf(&path, pages, &format!("{} Page Document", pages))?;
println!("Created large/{}pages.pdf", pages);
}
// Vertical writing
let path = fixture_dir.join("vertical/vertical.pdf");
create_minimal_pdf(&path, "Vertical", "Vertical Text Document")?;
println!("Created vertical/vertical.pdf");
// Code
let path = fixture_dir.join("code/code.pdf");
create_minimal_pdf(&path, "function test() { return true; }", "Code Sample")?;
println!("Created code/code.pdf");
// XMP metadata
let path = fixture_dir.join("xmp/xmp-metadata.pdf");
create_minimal_pdf(&path, "XMP Document", "XMP Metadata Document")?;
println!("Created xmp/xmp-metadata.pdf");
// Receipts
let receipt_dir = fixture_dir.join("receipts");
fs::create_dir_all(&receipt_dir)?;
let mut file = File::create(receipt_dir.join("valid-receipt.receipt.json"))?;
file.write_all(br#"{"fingerprint": "stub-valid", "signature": "valid-signature"}"#)?;
let mut file = File::create(receipt_dir.join("tampered-receipt.receipt.json"))?;
file.write_all(br#"{"fingerprint": "stub-tampered", "signature": "invalid-signature"}"#)?;
create_minimal_pdf(&receipt_dir.join("valid-receipt.pdf"), "Valid Receipt", "Valid Receipt")?;
create_minimal_pdf(&receipt_dir.join("tampered-receipt.pdf"), "Tampered Receipt", "Tampered Receipt")?;
println!("Created receipt fixtures");
// Broken/corrupt PDF
let path = fixture_dir.join("broken/corrupt.pdf");
fs::create_dir_all(path.parent().unwrap())?;
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 stub fixtures created successfully!");
Ok(())
}