diora-web/e2e/fixtures/make_test_pdf.py

119 lines
5 KiB
Python
Raw Normal View History

PDF-Reader: ein Zeichenweg, Seitenmodus neu, Seitensprung für beide Modi (WIP, ungetestet) - 58-pdf.js: _pdfDraw – eine Zeichnung je Canvas (die vorige wird abgebrochen und abgewartet), Pixeldichte, höchstens 16,7 MP; Seiten, die gehen, geben auch pdf.js' Seitendaten frei; ein gemeinsamer pdf.js-Worker (_pdfLoad) auch für Uploads, Seitenzahl, Hefte und Cover; Inhalts- verzeichnis parallel aufgelöst, auch mit Seitenindex als Ziel; ResizeObserver: Drehen, Breite, Leisten – neu an derselben Seite. - 66-pdf-paginated.js: Seitenmodus auf _pdfDraw, Zuschnitt in Bildschirmkoordinaten (gedrehte Seiten) und höchstens 2,5× der ganzen Seite (eine fast leere Seite wird nicht unendlich groß); nächste und vorige Seite vorgezeichnet; Tippen zum Blättern blendet die Leisten nicht mehr ein; Wischen nur deutlich seitwärts; Mausrad; Pinch setzt die Vorschau zurück; nach dem Blättern wird gespeichert und 'diora-pdf-turn' gemeldet (E-Ink-Refresh). - pdfGoToPage für beide Modi: Inhaltsverzeichnis, Seitenzahl im Kopf, Lesezeichen, Tasten (Seitenmodus jetzt auch Leertaste, Bild auf/ab). - Beim Scrollen wird die aktuelle Seite mitgeführt (Lesezeichen sagten immer „Seite 1“). - Schalter Scrollen | Seiten im PDF-Panel; Zoom-Regler zeichnet erst neu, wenn er ruht. - openBook hängt seine Listener über _readerListen an; closeReader nimmt sie ab (vorher sammelten sie sich bei jedem Öffnen an). Abbrechen und ein zweites Buch geben das alte Dokument frei; der entschlüsselte PDF-Puffer wird nicht mehr gehalten. - Invertieren dreht die Farbtöne zurück (Fotos keine Negative). - e2e/fixtures/make_test_pdf.py: Test-PDF mit gemischten Formaten, Outline, Links und einer fast leeren Seite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:52:28 +01:00
"""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')