119 lines
5 KiB
Python
119 lines
5 KiB
Python
|
|
"""A small PDF with the cases the PDF reader has to get right (e2e and by hand).
|
||
|
|
|
||
|
|
python e2e/fixtures/make_test_pdf.py e2e/fixtures/test.pdf
|
||
|
|
|
||
|
|
12 pages: A4 portrait text pages, one A5 page and one A4 landscape page (mixed
|
||
|
|
sizes), page 7 nearly empty (only a page number: the smart crop must not zoom
|
||
|
|
it to infinity), an outline (table of contents) with three entries, internal
|
||
|
|
links from page 1 to pages 5 and 9 and one external link. Plain PDF 1.4,
|
||
|
|
Helvetica, no dependencies.
|
||
|
|
"""
|
||
|
|
import sys
|
||
|
|
|
||
|
|
A4 = (595, 842)
|
||
|
|
A5 = (420, 595)
|
||
|
|
A4L = (842, 595)
|
||
|
|
|
||
|
|
WORDS = ('Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. '
|
||
|
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. ').split()
|
||
|
|
|
||
|
|
|
||
|
|
def esc(t):
|
||
|
|
return t.replace('\\', '\\\\').replace('(', '\\(').replace(')', '\\)')
|
||
|
|
|
||
|
|
|
||
|
|
def text_lines(n, words_per_line=11):
|
||
|
|
out, i = [], 0
|
||
|
|
for _ in range(n):
|
||
|
|
out.append(' '.join(WORDS[(i + k) % len(WORDS)] for k in range(words_per_line)))
|
||
|
|
i += words_per_line
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def page_content(no, size):
|
||
|
|
w, h = size
|
||
|
|
ops = ['BT', '/F1 22 Tf', f'50 {h - 70} Td', f'(Kapitel {no} - Seite {no}) Tj', 'ET']
|
||
|
|
if no == 7:
|
||
|
|
# Nearly empty: just the number at the bottom.
|
||
|
|
return '\n'.join(['BT', '/F1 10 Tf', f'{w / 2 - 10:.0f} 30 Td', f'({no}) Tj', 'ET'])
|
||
|
|
y = h - 110
|
||
|
|
lines = text_lines(28 if h > 700 else 18, 11 if w > 500 else 8)
|
||
|
|
ops += ['BT', '/F1 11 Tf', f'50 {y} Td', '14 TL']
|
||
|
|
for ln in lines:
|
||
|
|
ops.append(f'({esc(ln)}) Tj T*')
|
||
|
|
ops.append('ET')
|
||
|
|
if no == 1:
|
||
|
|
ops += ['BT', '/F1 12 Tf', f'50 {y - 30 - 14 * len(lines)} Td',
|
||
|
|
'(Link zu Seite 5) Tj', '0 -20 Td', '(Link zu Seite 9) Tj', '0 -20 Td', '(Externer Link: example.org) Tj', 'ET']
|
||
|
|
ops += ['BT', '/F1 10 Tf', f'{w / 2 - 10:.0f} 30 Td', f'({no}) Tj', 'ET']
|
||
|
|
return '\n'.join(ops)
|
||
|
|
|
||
|
|
|
||
|
|
def build(path):
|
||
|
|
sizes = [A4] * 12
|
||
|
|
sizes[3] = A5 # page 4
|
||
|
|
sizes[8] = A4L # page 9
|
||
|
|
objs = [] # index = object number - 1
|
||
|
|
|
||
|
|
def add(body):
|
||
|
|
objs.append(body)
|
||
|
|
return len(objs)
|
||
|
|
|
||
|
|
catalog = add(None)
|
||
|
|
pages_obj = add(None)
|
||
|
|
font = add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>')
|
||
|
|
page_ids = []
|
||
|
|
for i, size in enumerate(sizes):
|
||
|
|
content = page_content(i + 1, size).encode('cp1252')
|
||
|
|
cid = add(b'<< /Length %d >>\nstream\n' % len(content) + content + b'\nendstream')
|
||
|
|
pid = add(None)
|
||
|
|
page_ids.append((pid, cid, size))
|
||
|
|
# links on page 1 (placed where the text is: bottom part of the text block)
|
||
|
|
p1_h = sizes[0][1]
|
||
|
|
base_y = p1_h - 110 - 30 - 14 * 28
|
||
|
|
|
||
|
|
def link_to(page_no, y):
|
||
|
|
return add(f'<< /Type /Annot /Subtype /Link /Rect [48 {y - 4} 220 {y + 14}] /Border [0 0 0] '
|
||
|
|
f'/Dest [{page_ids[page_no - 1][0]} 0 R /XYZ 0 {sizes[page_no - 1][1]} null] >>')
|
||
|
|
annots = [link_to(5, base_y), link_to(9, base_y - 20),
|
||
|
|
add(f'<< /Type /Annot /Subtype /Link /Rect [48 {base_y - 44} 300 {base_y - 26}] /Border [0 0 0] '
|
||
|
|
f'/A << /S /URI /URI (https://example.org/) >> >>')]
|
||
|
|
for n, (pid, cid, (w, h)) in enumerate(page_ids):
|
||
|
|
extra = f' /Annots [{" ".join(f"{a} 0 R" for a in annots)}]' if n == 0 else ''
|
||
|
|
objs[pid - 1] = (f'<< /Type /Page /Parent {pages_obj} 0 R /MediaBox [0 0 {w} {h}] '
|
||
|
|
f'/Resources << /Font << /F1 {font} 0 R >> >> /Contents {cid} 0 R{extra} >>')
|
||
|
|
objs[pages_obj - 1] = f'<< /Type /Pages /Kids [{" ".join(f"{p} 0 R" for p, _, _ in page_ids)}] /Count {len(page_ids)} >>'
|
||
|
|
# outline: three entries
|
||
|
|
outline = add(None)
|
||
|
|
entries = [('Anfang', 1), ('Querformat', 9), ('Fast leer', 7)]
|
||
|
|
ids = [add(None) for _ in entries]
|
||
|
|
for k, ((title, page), oid) in enumerate(zip(entries, ids)):
|
||
|
|
nav = ''
|
||
|
|
if k > 0:
|
||
|
|
nav += f' /Prev {ids[k - 1]} 0 R'
|
||
|
|
if k < len(ids) - 1:
|
||
|
|
nav += f' /Next {ids[k + 1]} 0 R'
|
||
|
|
objs[oid - 1] = (f'<< /Title ({esc(title)}) /Parent {outline} 0 R{nav} '
|
||
|
|
f'/Dest [{page_ids[page - 1][0]} 0 R /XYZ 0 {sizes[page - 1][1]} null] >>')
|
||
|
|
objs[outline - 1] = f'<< /Type /Outlines /First {ids[0]} 0 R /Last {ids[-1]} 0 R /Count {len(ids)} >>'
|
||
|
|
objs[catalog - 1] = f'<< /Type /Catalog /Pages {pages_obj} 0 R /Outlines {outline} 0 R /PageMode /UseOutlines >>'
|
||
|
|
info = add('<< /Title (Testdokument PDF) /Author (diora Test) >>')
|
||
|
|
|
||
|
|
out = bytearray(b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n')
|
||
|
|
offsets = []
|
||
|
|
for i, body in enumerate(objs):
|
||
|
|
offsets.append(len(out))
|
||
|
|
data = body if isinstance(body, bytes) else body.encode('cp1252')
|
||
|
|
out += b'%d 0 obj\n' % (i + 1) + data + b'\nendobj\n'
|
||
|
|
xref = len(out)
|
||
|
|
out += b'xref\n0 %d\n0000000000 65535 f \n' % (len(objs) + 1)
|
||
|
|
for off in offsets:
|
||
|
|
out += b'%010d 00000 n \n' % off
|
||
|
|
out += (b'trailer\n<< /Size %d /Root %d 0 R /Info %d 0 R >>\nstartxref\n%d\n%%%%EOF\n'
|
||
|
|
% (len(objs) + 1, catalog, info, xref))
|
||
|
|
open(path, 'wb').write(out)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
build(sys.argv[1] if len(sys.argv) > 1 else 'test.pdf')
|