Files
pdf/tools/generate_corpus.py
2026-06-22 15:18:47 +05:30

189 lines
6.5 KiB
Python

"""
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
objects[obj_num] = None
catalog_num = obj_num
obj_num += 1
objects[obj_num] = None
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!')