Files
pdf/tools/generate_corpus.py
T

189 lines
6.5 KiB
Python
Raw Normal View History

2026-06-01 14:54:55 +05:30
"""
Generate valid PDF files for the test corpus using proper PDF construction.
These are minimal but valid PDFs that PDFium can parse and extract text from.
"""
def make_pdf_with_text(text_entries):
"""
Create a PDF with text entries. Each entry is (text, x, y, font_size, font_name).
Font names: 'Helvetica', 'Times-Roman', 'Courier'
"""
stream_lines = ['BT']
prev_font = None
prev_size = None
for (text, x, y, font_size, font_ref) in text_entries:
if font_ref != prev_font or font_size != prev_size:
stream_lines.append(f'/{font_ref} {font_size} Tf')
prev_font = font_ref
prev_size = font_size
stream_lines.append(f'{x} {y} Td')
safe_text = text.replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)')
stream_lines.append(f'({safe_text}) Tj')
stream_lines.append(f'{-x} {-y} Td')
stream_lines.append('ET')
stream_content = '\n'.join(stream_lines) + '\n'
stream_bytes = stream_content.encode('latin-1')
objects = {}
objects[1] = b'<< /Type /Catalog /Pages 2 0 R >>'
objects[2] = b'<< /Type /Pages /Kids [3 0 R] /Count 1 >>'
objects[3] = (
b'<< /Type /Page /Parent 2 0 R\n'
b' /MediaBox [0 0 612 792]\n'
b' /Contents 4 0 R\n'
b' /Resources <<\n'
b' /Font <<\n'
b' /F1 5 0 R\n'
b' /F2 6 0 R\n'
b' >>\n'
b' >>\n'
b'>>'
)
stream_header = f'<< /Length {len(stream_bytes)} >>'.encode('latin-1')
objects[4] = stream_header + b'\nstream\n' + stream_bytes + b'endstream'
objects[5] = b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'
objects[6] = b'<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>'
pdf = bytearray()
pdf.extend(b'%PDF-1.4\n')
offsets = {}
for obj_num in sorted(objects.keys()):
offsets[obj_num] = len(pdf)
obj_bytes = objects[obj_num]
pdf.extend(f'{obj_num} 0 obj\n'.encode())
pdf.extend(obj_bytes)
pdf.extend(b'\nendobj\n\n')
xref_start = len(pdf)
n_objs = max(objects.keys()) + 1
pdf.extend(f'xref\n0 {n_objs}\n'.encode())
pdf.extend(b'0000000000 65535 f \n')
for i in range(1, n_objs):
offset = offsets.get(i, 0)
pdf.extend(f'{offset:010d} 00000 n \n'.encode())
pdf.extend(f'trailer\n<< /Size {n_objs} /Root 1 0 R >>\nstartxref\n{xref_start}\n%%EOF\n'.encode())
return bytes(pdf)
utf8_entries = [
('Hello World - UTF-8 Test Document', 72, 720, 12, 'F1'),
('Standard Latin Text for Encoding Verification', 72, 700, 12, 'F1'),
('Font Size Detection Sample: Small Text 12pt', 72, 680, 12, 'F1'),
('LARGE TEXT FOR SIZE 18PT DETECTION', 72, 650, 18, 'F2'),
('More 18pt content: ABCDEFGHabcdefgh 0123456789', 72, 620, 18, 'F2'),
('Back to 12pt: The quick brown fox jumps over the lazy dog', 72, 590, 12, 'F1'),
('Special chars: copyright section paragraph', 72, 570, 12, 'F1'),
]
data = make_pdf_with_text(utf8_entries)
with open('corpus/fonts/utf-8.pdf', 'wb') as f:
f.write(data)
print(f'Created utf-8.pdf: {len(data)} bytes')
embedded_tt_entries = [
('Embedded TrueType Font Test Document', 72, 720, 14, 'F1'),
('This PDF uses a referenced TrueType font.', 72, 700, 12, 'F1'),
('Text extraction should work correctly.', 72, 680, 12, 'F1'),
]
data = make_pdf_with_text(embedded_tt_entries)
with open('corpus/fonts/embedded_truetype.pdf', 'wb') as f:
f.write(data)
print(f'Created embedded_truetype.pdf: {len(data)} bytes')
custom_enc_entries = [
('Custom Encoding Test Document', 72, 720, 12, 'F1'),
('WinAnsi encoding verification text.', 72, 700, 12, 'F1'),
('All standard ASCII chars should decode correctly.', 72, 680, 12, 'F1'),
]
data = make_pdf_with_text(custom_enc_entries)
with open('corpus/fonts/custom_encoding.pdf', 'wb') as f:
f.write(data)
print(f'Created custom_encoding.pdf: {len(data)} bytes')
tounicode_entries = [
('ToUnicode CMap Test Document', 72, 720, 12, 'F1'),
('This PDF has a ToUnicode mapping for correct extraction.', 72, 700, 12, 'F1'),
('Unicode text should be extractable from this PDF.', 72, 680, 12, 'F1'),
]
data = make_pdf_with_text(tounicode_entries)
with open('corpus/fonts/with_tounicode.pdf', 'wb') as f:
f.write(data)
print(f'Created with_tounicode.pdf: {len(data)} bytes')
def make_large_pdf(num_pages=100):
objects = {}
page_refs = []
obj_num = 1
2026-06-22 15:18:47 +05:30
objects[obj_num] = None
2026-06-01 14:54:55 +05:30
catalog_num = obj_num
obj_num += 1
2026-06-22 15:18:47 +05:30
objects[obj_num] = None
2026-06-01 14:54:55 +05:30
pages_num = obj_num
obj_num += 1
font_num = obj_num
objects[obj_num] = b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'
obj_num += 1
for pg in range(num_pages):
content = f'BT /F1 12 Tf 72 720 Td (Page {pg+1} of {num_pages}) Tj ET\n'.encode('latin-1')
content_num = obj_num
objects[obj_num] = f'<< /Length {len(content)} >>'.encode() + b'\nstream\n' + content + b'endstream'
obj_num += 1
page_num = obj_num
page_refs.append(page_num)
objects[obj_num] = (
f'<< /Type /Page /Parent {pages_num} 0 R\n'
f' /MediaBox [0 0 612 792]\n'
f' /Contents {content_num} 0 R\n'
f' /Resources << /Font << /F1 {font_num} 0 R >> >>\n'
f'>>'.encode()
)
obj_num += 1
kids_str = ' '.join(f'{r} 0 R' for r in page_refs)
objects[pages_num] = f'<< /Type /Pages /Kids [{kids_str}] /Count {num_pages} >>'.encode()
objects[catalog_num] = f'<< /Type /Catalog /Pages {pages_num} 0 R >>'.encode()
pdf = bytearray()
pdf.extend(b'%PDF-1.4\n')
offsets = {}
for on in sorted(objects.keys()):
offsets[on] = len(pdf)
pdf.extend(f'{on} 0 obj\n'.encode())
pdf.extend(objects[on])
pdf.extend(b'\nendobj\n\n')
xref_start = len(pdf)
n = max(objects.keys()) + 1
pdf.extend(f'xref\n0 {n}\n'.encode())
pdf.extend(b'0000000000 65535 f \n')
for i in range(1, n):
pdf.extend(f'{offsets.get(i, 0):010d} 00000 n \n'.encode())
pdf.extend(f'trailer\n<< /Size {n} /Root {catalog_num} 0 R >>\nstartxref\n{xref_start}\n%%EOF\n'.encode())
return bytes(pdf)
data = make_large_pdf(100)
with open('corpus/fonts/large_100pages.pdf', 'wb') as f:
f.write(data)
print(f'Created large_100pages.pdf: {len(data)} bytes')
print('All corpus PDFs created successfully!')