"""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')