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>
129 lines
5.8 KiB
Python
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')
|