diora-web/e2e/fixtures/make_test_pdf.py
mrwnslz 429b875faf
Some checks failed
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 11m30s
Test / e2e (push) Failing after 1m30s
PDF: öffnet an der gespeicherten Seite, Leserfarben, Seitenzahlen wie im Buch (sw CACHE v166)
Der Lesestand wird gleich beim Öffnen erfragt (parallel zur Datei), ein PDF zeichnet zuerst
die Seite, an der es weitergeht, statt Seite 1; Titel, Inhaltsverzeichnis und Seitenzahlen
kommen nebenher und halten das Öffnen nicht mehr auf. Eine Zeichnung, die schon läuft, wird
abgewartet statt neu begonnen.

„Leserfarben“ (neben Originalfarben und Invertieren): die Seite in den Farben des Lesers
(Dunkel, Sepia) per SVG-Filter, Bilder darüber wie gedruckt (ihre Lage aus den
Zeichenbefehlen); ein Bild, das die halbe Seite füllt, ist ein Scan und wird mit umgefärbt.
pdf.js' eigene pageColors färben die Bilder mit um. Ein anderes Design ändert nur die Farben
des Filters.

Seitenzahlen des Buchs (PageLabels: „iv“, „A-3“) im Kopf mit der Seite der Datei daneben,
eingeben lässt sich beides; Lesezeichen und der Zurück-Knopf nennen sie. Das Test-PDF hat
dafür Seitenzahlen und ein farbiges Bild bekommen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:42:26 +01:00

129 lines
5.8 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, a coloured picture on
page 2 (it must stay as it is in the reader's colours) and page labels like a
book's: i, ii for the first two pages, then 1, 2, 3 … 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 == 2:
ops += ['q', f'200 0 0 120 {w - 260} 60 cm', '/Im1 Do', 'Q']
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 >>')
# A picture of four colours (2 x 2 pixels, drawn large).
pixels = b'FF0000 00A000 0000FF FFD700>'
image = add(b'<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB '
b'/BitsPerComponent 8 /Filter /ASCIIHexDecode /Length %d >>\nstream\n' % len(pixels)
+ pixels + b'\nendstream')
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 ''
xobj = f' /XObject << /Im1 {image} 0 R >>' if n == 1 else ''
objs[pid - 1] = (f'<< /Type /Page /Parent {pages_obj} 0 R /MediaBox [0 0 {w} {h}] '
f'/Resources << /Font << /F1 {font} 0 R >>{xobj} >> /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 '
f'/PageLabels << /Nums [0 << /S /r >> 2 << /S /D >>] >> >>')
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')