diff --git a/e2e/fixtures/make_test_pdf.py b/e2e/fixtures/make_test_pdf.py
new file mode 100644
index 0000000..b306acb
--- /dev/null
+++ b/e2e/fixtures/make_test_pdf.py
@@ -0,0 +1,118 @@
+"""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')
diff --git a/e2e/fixtures/test.pdf b/e2e/fixtures/test.pdf
new file mode 100644
index 0000000..e09c0a2
--- /dev/null
+++ b/e2e/fixtures/test.pdf
@@ -0,0 +1,641 @@
+%PDF-1.4
+%âãÏÓ
+1 0 obj
+<< /Type /Catalog /Pages 2 0 R /Outlines 31 0 R /PageMode /UseOutlines >>
+endobj
+2 0 obj
+<< /Type /Pages /Kids [5 0 R 7 0 R 9 0 R 11 0 R 13 0 R 15 0 R 17 0 R 19 0 R 21 0 R 23 0 R 25 0 R 27 0 R] /Count 12 >>
+endobj
+3 0 obj
+<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
+endobj
+4 0 obj
+<< /Length 2463 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 1 - Seite 1) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 12 Tf
+50 310 Td
+(Link zu Seite 5) Tj
+0 -20 Td
+(Link zu Seite 9) Tj
+0 -20 Td
+(Externer Link: example.org) Tj
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(1) Tj
+ET
+endstream
+endobj
+5 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 4 0 R /Annots [28 0 R 29 0 R 30 0 R] >>
+endobj
+6 0 obj
+<< /Length 2345 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 2 - Seite 2) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(2) Tj
+ET
+endstream
+endobj
+7 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 6 0 R >>
+endobj
+8 0 obj
+<< /Length 2345 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 3 - Seite 3) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(3) Tj
+ET
+endstream
+endobj
+9 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 8 0 R >>
+endobj
+10 0 obj
+<< /Length 1196 >>
+stream
+BT
+/F1 22 Tf
+50 525 Td
+(Kapitel 4 - Seite 4) Tj
+ET
+BT
+/F1 11 Tf
+50 485 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+ET
+BT
+/F1 10 Tf
+200 30 Td
+(4) Tj
+ET
+endstream
+endobj
+11 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 420 595] /Resources << /Font << /F1 3 0 R >> >> /Contents 10 0 R >>
+endobj
+12 0 obj
+<< /Length 2345 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 5 - Seite 5) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(5) Tj
+ET
+endstream
+endobj
+13 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 12 0 R >>
+endobj
+14 0 obj
+<< /Length 2345 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 6 - Seite 6) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(6) Tj
+ET
+endstream
+endobj
+15 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 14 0 R >>
+endobj
+16 0 obj
+<< /Length 32 >>
+stream
+BT
+/F1 10 Tf
+288 30 Td
+(7) Tj
+ET
+endstream
+endobj
+17 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 16 0 R >>
+endobj
+18 0 obj
+<< /Length 2345 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 8 - Seite 8) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(8) Tj
+ET
+endstream
+endobj
+19 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 18 0 R >>
+endobj
+20 0 obj
+<< /Length 1547 >>
+stream
+BT
+/F1 22 Tf
+50 525 Td
+(Kapitel 9 - Seite 9) Tj
+ET
+BT
+/F1 11 Tf
+50 485 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+ET
+BT
+/F1 10 Tf
+411 30 Td
+(9) Tj
+ET
+endstream
+endobj
+21 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 842 595] /Resources << /Font << /F1 3 0 R >> >> /Contents 20 0 R >>
+endobj
+22 0 obj
+<< /Length 2348 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 10 - Seite 10) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(10) Tj
+ET
+endstream
+endobj
+23 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 22 0 R >>
+endobj
+24 0 obj
+<< /Length 2348 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 11 - Seite 11) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(11) Tj
+ET
+endstream
+endobj
+25 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 24 0 R >>
+endobj
+26 0 obj
+<< /Length 2348 >>
+stream
+BT
+/F1 22 Tf
+50 772 Td
+(Kapitel 12 - Seite 12) Tj
+ET
+BT
+/F1 11 Tf
+50 732 Td
+14 TL
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+(etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed) Tj T*
+(do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und) Tj T*
+(Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit,) Tj T*
+(sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche) Tj T*
+(und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur adipiscing) Tj T*
+(elit, sed do eiusmod tempor. Die Seite trägt Text, damit Auswahl,) Tj T*
+(Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet, consectetur) Tj T*
+(adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text, damit) Tj T*
+(Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit amet,) Tj T*
+(consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt Text,) Tj T*
+(damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor sit) Tj T*
+(amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite trägt) Tj T*
+(Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum dolor) Tj T*
+(sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die Seite) Tj T*
+(trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem ipsum) Tj T*
+(dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor. Die) Tj T*
+(Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden. Lorem) Tj T*
+(ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.) Tj T*
+(Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas finden.) Tj T*
+(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod) Tj T*
+(tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen etwas) Tj T*
+(finden. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do) Tj T*
+(eiusmod tempor. Die Seite trägt Text, damit Auswahl, Suche und Vorlesen) Tj T*
+ET
+BT
+/F1 10 Tf
+288 30 Td
+(12) Tj
+ET
+endstream
+endobj
+27 0 obj
+<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents 26 0 R >>
+endobj
+28 0 obj
+<< /Type /Annot /Subtype /Link /Rect [48 306 220 324] /Border [0 0 0] /Dest [13 0 R /XYZ 0 842 null] >>
+endobj
+29 0 obj
+<< /Type /Annot /Subtype /Link /Rect [48 286 220 304] /Border [0 0 0] /Dest [21 0 R /XYZ 0 595 null] >>
+endobj
+30 0 obj
+<< /Type /Annot /Subtype /Link /Rect [48 266 300 284] /Border [0 0 0] /A << /S /URI /URI (https://example.org/) >> >>
+endobj
+31 0 obj
+<< /Type /Outlines /First 32 0 R /Last 34 0 R /Count 3 >>
+endobj
+32 0 obj
+<< /Title (Anfang) /Parent 31 0 R /Next 33 0 R /Dest [5 0 R /XYZ 0 842 null] >>
+endobj
+33 0 obj
+<< /Title (Querformat) /Parent 31 0 R /Prev 32 0 R /Next 34 0 R /Dest [21 0 R /XYZ 0 595 null] >>
+endobj
+34 0 obj
+<< /Title (Fast leer) /Parent 31 0 R /Prev 33 0 R /Dest [17 0 R /XYZ 0 842 null] >>
+endobj
+35 0 obj
+<< /Title (Testdokument PDF) /Author (diora Test) >>
+endobj
+xref
+0 36
+0000000000 65535 f
+0000000015 00000 n
+0000000104 00000 n
+0000000237 00000 n
+0000000334 00000 n
+0000002849 00000 n
+0000003006 00000 n
+0000005403 00000 n
+0000005529 00000 n
+0000007926 00000 n
+0000008052 00000 n
+0000009301 00000 n
+0000009429 00000 n
+0000011827 00000 n
+0000011955 00000 n
+0000014353 00000 n
+0000014481 00000 n
+0000014564 00000 n
+0000014692 00000 n
+0000017090 00000 n
+0000017218 00000 n
+0000018818 00000 n
+0000018946 00000 n
+0000021347 00000 n
+0000021475 00000 n
+0000023876 00000 n
+0000024004 00000 n
+0000026405 00000 n
+0000026533 00000 n
+0000026653 00000 n
+0000026773 00000 n
+0000026907 00000 n
+0000026981 00000 n
+0000027077 00000 n
+0000027191 00000 n
+0000027291 00000 n
+trailer
+<< /Size 36 /Root 1 0 R /Info 35 0 R >>
+startxref
+27360
+%%EOF
diff --git a/static/css/app.css b/static/css/app.css
index 9c6f893..40233a5 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -1812,7 +1812,8 @@ body.dnd-mode .timer-display {
.pdf-text-layer span.reader-search-match.active { background:rgba(230,57,70,.6); }
/* PDF invert */
-#reader-overlay.pdf-inverted .pdf-page { filter:invert(1); }
+/* Dark pages: inverted, and the hues turned back, so photos and coloured figures aren't negatives. */
+#reader-overlay.pdf-inverted .pdf-page { filter:invert(1) hue-rotate(180deg); }
/* ---- Immersive reader mode ---- */
.navbar { transition: transform 0.35s ease; }
diff --git a/static/js/app/26-sidebar.js b/static/js/app/26-sidebar.js
index 7d9de86..254b2ba 100644
--- a/static/js/app/26-sidebar.js
+++ b/static/js/app/26-sidebar.js
@@ -37,9 +37,12 @@ document.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); readerSearchNext(); }
if (e.key === 'ArrowUp') { e.preventDefault(); readerSearchPrev(); }
}
- if (readerSettings.pdfPaginated && currentPdfDoc) {
- if (e.key === 'ArrowRight') { e.preventDefault(); pdfGoToPage(pdfCurrentPage + 1); }
- if (e.key === 'ArrowLeft') { e.preventDefault(); pdfGoToPage(pdfCurrentPage - 1); }
+ // PDF page by page: arrows, Space, PageUp/PageDown turn (scrolling has its own keys below).
+ if (readerSettings.pdfPaginated && currentPdfDoc && !readerSearchOpen
+ && !/^(INPUT|TEXTAREA|SELECT)$/.test((document.activeElement && document.activeElement.tagName) || '')) {
+ const turn = {ArrowRight: 1, ArrowDown: 1, PageDown: 1, ' ': e.shiftKey ? -1 : 1,
+ ArrowLeft: -1, ArrowUp: -1, PageUp: -1}[e.key];
+ if (turn) { e.preventDefault(); pdfGoToPage(pdfCurrentPage + turn); return; }
}
// Vim-style scroll — ignore when typing in an input
diff --git a/static/js/app/40-epub.js b/static/js/app/40-epub.js
index dea34dc..0a765a8 100644
--- a/static/js/app/40-epub.js
+++ b/static/js/app/40-epub.js
@@ -106,10 +106,12 @@ async function parseEpub(arrayBuffer) {
// spine text by CHARS_PER_BOOK_PAGE (as the reader estimates it). The library's length sorts.
async function bookPagesOfFile(buf, type) {
if (type === 'pdf') {
- const pdf = await pdfjsLib.getDocument({data: new Uint8Array(buf)}).promise;
- const n = pdf.numPages;
- pdf.destroy();
- return n;
+ const pdf = await _pdfLoad(new Uint8Array(buf)).promise;
+ try {
+ return pdf.numPages;
+ } finally {
+ try { pdf.destroy(); } catch (e) {}
+ }
}
const zip = await JSZip.loadAsync(buf);
const containerXml = await zip.file('META-INF/container.xml').async('text');
diff --git a/static/js/app/52-book-list.js b/static/js/app/52-book-list.js
index ff3f49d..e5003b9 100644
--- a/static/js/app/52-book-list.js
+++ b/static/js/app/52-book-list.js
@@ -133,10 +133,14 @@ function repairBook(bookId) {
let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = '', isbn = '';
try {
if (isPdf) {
- const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise;
- const meta = await pdfDoc.getMetadata();
- title = meta.info?.Title?.trim() || title;
- author = meta.info?.Author?.trim() || '';
+ const pdfDoc = await _pdfLoad(new Uint8Array(buf.slice(0))).promise;
+ try {
+ const meta = await pdfDoc.getMetadata();
+ title = meta.info?.Title?.trim() || title;
+ author = meta.info?.Author?.trim() || '';
+ } finally {
+ try { pdfDoc.destroy(); } catch (e) {}
+ }
} else {
const zip = await JSZip.loadAsync(buf.slice(0));
const containerXml = await zip.file('META-INF/container.xml').async('text');
@@ -785,10 +789,14 @@ async function _importEbookBuffer(buf, filename, statusEl, opts) {
if (isPdf) {
try {
- const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise;
- const meta = await pdfDoc.getMetadata();
- title = meta.info?.Title?.trim() || title;
- author = meta.info?.Author?.trim() || '';
+ const pdfDoc = await _pdfLoad(new Uint8Array(buf.slice(0))).promise;
+ try {
+ const meta = await pdfDoc.getMetadata();
+ title = meta.info?.Title?.trim() || title;
+ author = meta.info?.Author?.trim() || '';
+ } finally {
+ try { pdfDoc.destroy(); } catch (e) {}
+ }
} catch (e) { /* use filename as title */ }
} else {
try {
diff --git a/static/js/app/54-issues.js b/static/js/app/54-issues.js
index 6b34c29..13d58d5 100644
--- a/static/js/app/54-issues.js
+++ b/static/js/app/54-issues.js
@@ -279,7 +279,7 @@ const _issueNorm = s => String(s || '').toLowerCase().replace(/ß/g, 'ss').norma
async function _issueIndexPdf(bytes) {
if (typeof pdfjsLib === 'undefined') return null;
- const pdf = await pdfjsLib.getDocument({data: new Uint8Array(bytes)}).promise;
+ const pdf = await _pdfLoad(new Uint8Array(bytes)).promise;
try {
return await _issuePdfArticles(pdf);
} finally {
diff --git a/static/js/app/58-pdf.js b/static/js/app/58-pdf.js
index 375f688..78cd183 100644
--- a/static/js/app/58-pdf.js
+++ b/static/js/app/58-pdf.js
@@ -1,49 +1,145 @@
'use strict';
+// ---------------------------------------------------------------------------
+// The PDF reader: one document, shown either scrolling (every page has its
+// place, only those near the view are drawn) or page by page, cropped to the
+// page's text (66-pdf-paginated.js). Whichever, a page is drawn only through
+// _pdfDraw: one drawing at a time per canvas, sharp at the device's pixel
+// ratio but never larger than PDF_MAX_PIXELS.
+// ---------------------------------------------------------------------------
+
+// pdf.js's own viewer caps a canvas at this many pixels: beyond it phones run out of memory.
+const PDF_MAX_PIXELS = 16777216;
+
+// One pdf.js worker for every document (the reader, covers, uploads): starting one takes a
+// moment — on an e-reader a long one.
+let _pdfWorker = null;
+
+function _pdfSharedWorker() {
+ if (!_pdfWorker && typeof pdfjsLib !== 'undefined' && pdfjsLib.PDFWorker) {
+ try { _pdfWorker = new pdfjsLib.PDFWorker(); } catch (e) { _pdfWorker = null; }
+ }
+ return _pdfWorker;
+}
+
+/** pdf.js's loading task for [bytes] (a Uint8Array), on the shared worker. */
+function _pdfLoad(bytes) {
+ const worker = _pdfSharedWorker();
+ return pdfjsLib.getDocument(worker ? {data: bytes, worker} : {data: bytes});
+}
+
+// The table of contents: all entries resolved at once (one after the other cost a round trip
+// to the worker each). A destination's page is a reference or, in some files, a plain index.
async function _parsePdfOutline(pdf, items, depth) {
depth = depth || 0;
- const result = [];
- for (const item of items) {
+ const parts = await Promise.all(items.map(async item => {
let href = '';
if (item.dest) {
try {
const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
- if (dest) {
- const pageIndex = await pdf.getPageIndex(dest[0]);
- href = `#pdf-page-${pageIndex + 1}`;
+ if (dest && dest[0] != null) {
+ const index = typeof dest[0] === 'number' ? dest[0] : await pdf.getPageIndex(dest[0]);
+ href = `#pdf-page-${index + 1}`;
}
} catch (e) {}
}
- result.push({label: item.title || '(untitled)', href, depth});
- if (item.items && item.items.length) {
- result.push(...await _parsePdfOutline(pdf, item.items, depth + 1));
- }
- }
- return result;
+ const kids = item.items && item.items.length ? await _parsePdfOutline(pdf, item.items, depth + 1) : [];
+ return [{label: item.title || '(untitled)', href, depth}].concat(kids);
+ }));
+ return [].concat.apply([], parts);
}
// The document being loaded (pdf.js loading task), so opening can be cancelled.
let _pdfLoadingTask = null;
-// The pages' drawing of the last renderPdf: an observer and its queue (stopped on the next one).
+// The open document's pages and how they are drawn (renderPdf; stopped on the next one).
+let _pdfView = null;
+// Backwards name: the reader stops "the lazy drawing" when a book closes.
let _pdfLazy = null;
function _pdfStopLazy() {
- if (_pdfLazy) {
- _pdfLazy.stopped = true;
- if (_pdfLazy.observer) _pdfLazy.observer.disconnect();
- _pdfLazy = null;
+ const v = _pdfView;
+ if (!v) return;
+ v.stopped = true;
+ if (v.observer) v.observer.disconnect();
+ if (v.resize) v.resize.disconnect();
+ clearTimeout(v.resizeTimer);
+ v.entries.forEach(e => { e.token++; if (e.task) { try { e.task.cancel(); } catch (err) {} } });
+ _pdfView = _pdfLazy = null;
+}
+
+/**
+ * Draw page [page] of entry [e] at [cssScale] (CSS pixels per PDF point). Cancels a drawing
+ * of the same canvas still under way (pdf.js refuses two at once) and waits for it to let go.
+ * Returns false when a newer drawing (or letting go) came first.
+ */
+async function _pdfDraw(e, page, cssScale) {
+ const token = ++e.token;
+ const prev = e.task;
+ if (prev) {
+ try { prev.cancel(); } catch (err) {}
+ try { await prev.promise; } catch (err) {}
+ if (e.token !== token) return false;
}
+ const viewport = page.getViewport({scale: cssScale});
+ let ratio = window.devicePixelRatio || 1;
+ const area = viewport.width * viewport.height;
+ if (area * ratio * ratio > PDF_MAX_PIXELS) ratio = Math.max(0.5, Math.sqrt(PDF_MAX_PIXELS / area));
+ e.canvas.width = Math.round(viewport.width * ratio);
+ e.canvas.height = Math.round(viewport.height * ratio);
+ e.canvas.style.width = viewport.width + 'px';
+ e.canvas.style.height = viewport.height + 'px';
+ const task = page.render({
+ canvasContext: e.canvas.getContext('2d'),
+ viewport,
+ transform: ratio !== 1 ? [ratio, 0, 0, ratio, 0, 0] : null,
+ });
+ e.task = task;
+ try {
+ await task.promise;
+ } catch (err) {
+ if (e.task === task) e.task = null;
+ if (err && err.name !== 'RenderingCancelledException') console.warn('PDF page', e.num, err);
+ return false;
+ }
+ if (e.task === task) e.task = null;
+ if (e.token !== token) return false;
+ e.drawn = cssScale;
+ return true;
+}
+
+/** Let a page's pixels go (its place stays) and pdf.js forget what it parsed of it. */
+function _pdfLetGo(e, pdf) {
+ e.token++;
+ if (e.task) { try { e.task.cancel(); } catch (err) {} }
+ e.drawn = 0;
+ e.canvas.width = 0;
+ e.canvas.height = 0;
+ if (pdf) pdf.getPage(e.num).then(p => { if (!e.task) p.cleanup(); }, () => {});
+}
+
+/** The page shown at the top of the scrolling view (binary search, the pages are in order). */
+function _pdfTopPage(contentEl) {
+ const v = _pdfView;
+ if (!v || !v.entries.length) return pdfCurrentPage || 1;
+ const top = contentEl.getBoundingClientRect().top + 20;
+ let lo = 0, hi = v.entries.length - 1;
+ while (lo < hi) {
+ const mid = (lo + hi) >> 1;
+ if (v.entries[mid].wrapper.getBoundingClientRect().bottom > top) hi = mid; else lo = mid + 1;
+ }
+ return v.entries[lo].num;
}
// Every page gets its place at once (sized like the first page until it is drawn), but only the
// pages near the view are drawn; ones far away are let go again. A book of 1000 scanned pages
-// opens as fast as one of ten, and doesn't hold 1000 canvases.
+// opens as fast as one of ten, and doesn't hold 1000 canvases. Page by page (pdfPaginated),
+// 66-pdf-paginated.js shows and draws the pages itself.
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
const myGen = ++_pdfRenderGen;
_pdfStopLazy();
let pdf = currentPdfDoc;
if (!pdf) {
- _pdfLoadingTask = pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))});
+ _pdfLoadingTask = _pdfLoad(new Uint8Array(arrayBuffer.slice(0)));
try {
pdf = await _pdfLoadingTask.promise;
} finally {
@@ -77,7 +173,6 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
? contentEl.clientWidth - 32
: Math.min(contentEl.clientWidth - 32, 900);
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
- const dpr = window.devicePixelRatio || 1;
const scaleFor = vp => (scaleOverride != null ? scaleOverride
: Math.max(0.5, pageWidth / vp.width) * (readerSettings.pdfZoom / 100));
@@ -102,7 +197,7 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
canvas.style.height = firstVp.height + 'px';
inner.appendChild(canvas);
wrapper.appendChild(inner);
- if (!readerSettings.pdfSpread) {
+ if (!readerSettings.pdfSpread || readerSettings.pdfPaginated) {
pdfVp.appendChild(wrapper);
} else if (pageNum === 1) {
wrapper.classList.add('pdf-spread-cover');
@@ -117,78 +212,86 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
if (spreadContainer) spreadContainer.appendChild(wrapper);
else pdfVp.appendChild(wrapper);
}
- entries.push({num: pageNum, wrapper, canvas, drawn: false, near: false, task: null});
+ entries.push({num: pageNum, wrapper, inner, canvas, drawn: 0, near: false, task: null, token: 0});
}
pdfTotalPages = pdf.numPages;
-
const pivot = Math.max(1, Math.min(pdf.numPages, pivotPage || 1));
- const pivotEl = document.getElementById(`pdf-page-${pivot}`);
- if (pivotEl && pivot > 1) pivotEl.scrollIntoView({block: 'start'});
- const lazy = {stopped: false, observer: null, queue: new Set(), working: false};
- _pdfLazy = lazy;
- const gone = () => lazy.stopped || _pdfRenderGen !== myGen;
+ const view = {pdf, contentEl, entries, gen: myGen, stopped: false, observer: null, resize: null,
+ resizeTimer: 0, queue: new Set(), working: false, scaleFor, showToken: 0};
+ _pdfView = _pdfLazy = view;
+ const gone = () => view.stopped || _pdfRenderGen !== myGen;
async function draw(e) {
const page = await pdf.getPage(e.num);
- if (gone() || !e.near) return;
- const viewport = page.getViewport({scale: scaleFor(page.getViewport({scale: 1}))});
- e.canvas.style.width = viewport.width + 'px';
- e.canvas.style.height = viewport.height + 'px';
- e.canvas.width = Math.round(viewport.width * dpr);
- e.canvas.height = Math.round(viewport.height * dpr);
- const ctx = e.canvas.getContext('2d');
- ctx.scale(dpr, dpr);
- e.task = page.render({canvasContext: ctx, viewport});
- try {
- await e.task.promise;
- e.drawn = true;
- } catch (err) { /* cancelled: let go below */ }
- e.task = null;
- }
-
- function letGo(e) {
- if (e.task) { try { e.task.cancel(); } catch (err) {} }
- if (!e.drawn) return;
- e.drawn = false;
- e.canvas.width = 0; // frees the pixels; the CSS size keeps the page's place
- e.canvas.height = 0;
+ if (gone() || !e.near || readerSettings.pdfPaginated) return;
+ await _pdfDraw(e, page, scaleFor(page.getViewport({scale: 1})));
}
// One page at a time, the nearest to the current one first.
async function work() {
- if (lazy.working) return;
- lazy.working = true;
+ if (view.working) return;
+ view.working = true;
try {
- while (!gone() && lazy.queue.size) {
- const cur = typeof _currentScrollPage === 'function' && !readerSettings.pdfPaginated
- ? _currentScrollPage(contentEl) : (pdfCurrentPage || pivot);
- const next = Array.from(lazy.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
- lazy.queue.delete(next);
+ while (!gone() && view.queue.size && !readerSettings.pdfPaginated) {
+ const cur = _pdfTopPage(contentEl);
+ const next = Array.from(view.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
+ view.queue.delete(next);
if (next.near && !next.drawn) await draw(next);
}
} finally {
- lazy.working = false;
+ view.working = false;
}
}
+ // Turning the device, a narrower window, the bars coming or going: laid out again at the same page.
+ let lastW = contentEl.clientWidth, lastH = contentEl.clientHeight;
+ if (typeof ResizeObserver === 'function') {
+ view.resize = new ResizeObserver(() => {
+ const w = contentEl.clientWidth, h = contentEl.clientHeight;
+ if (!w || !h || (w === lastW && h === lastH)) return;
+ const widthChanged = Math.abs(w - lastW) > 2;
+ lastW = w;
+ lastH = h;
+ clearTimeout(view.resizeTimer);
+ view.resizeTimer = setTimeout(() => {
+ if (gone()) return;
+ if (readerSettings.pdfPaginated) _pdfShowPage(pdfCurrentPage);
+ else if (widthChanged) reRenderPdf(_pdfTopPage(contentEl));
+ }, readerSettings.pdfPaginated ? 150 : 300);
+ });
+ view.resize.observe(contentEl);
+ }
+
+ if (readerSettings.pdfPaginated) {
+ // Page by page: the pivot page cropped to its text, at once (66-pdf-paginated.js).
+ pdfCurrentPage = pivot;
+ await _pdfShowPage(pivot);
+ if (gone()) return null;
+ return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages};
+ }
+
+ if (pivot > 1) entries[pivot - 1].wrapper.scrollIntoView({block: 'start'});
+ pdfCurrentPage = pivot;
+
const byEl = new Map(entries.map(e => [e.wrapper, e]));
if (typeof IntersectionObserver === 'function') {
- lazy.observer = new IntersectionObserver(items => {
+ view.observer = new IntersectionObserver(items => {
+ if (readerSettings.pdfPaginated) return;
for (const it of items) {
const e = byEl.get(it.target);
if (!e) continue;
e.near = it.isIntersecting;
- if (e.near) { if (!e.drawn) lazy.queue.add(e); }
- else { lazy.queue.delete(e); letGo(e); }
+ if (e.near) { if (!e.drawn) view.queue.add(e); }
+ else { view.queue.delete(e); if (e.drawn || e.task) _pdfLetGo(e, pdf); }
}
work();
}, {root: contentEl, rootMargin: '150% 0px'});
- entries.forEach(e => lazy.observer.observe(e.wrapper));
+ entries.forEach(e => view.observer.observe(e.wrapper));
} else {
// Very old WebViews: every page, from the pivot on (as before).
- entries.forEach(e => { e.near = true; lazy.queue.add(e); });
+ entries.forEach(e => { e.near = true; view.queue.add(e); });
work();
}
diff --git a/static/js/app/62-reader.js b/static/js/app/62-reader.js
index 72d7a0a..8110f01 100644
--- a/static/js/app/62-reader.js
+++ b/static/js/app/62-reader.js
@@ -34,6 +34,22 @@ function _immHandleTap(e) {
// the text a click turns the page (63-pages.js), so only on the text itself.
let _readerFsHidBars = false;
+// What openBook hangs on the content and the page number for this book, taken off again in
+// closeReader — else every book opened adds its own, and a PDF's handlers run under the next
+// EPUB. (Not addEventListener's signal: the tolino's Chrome 61 ignores it.)
+let _readerListeners = [];
+
+function _readerListen(el, type, fn, opts) {
+ if (!el) return;
+ el.addEventListener(type, fn, opts);
+ _readerListeners.push([el, type, fn, opts]);
+}
+
+function _readerUnlistenAll() {
+ for (const [el, type, fn, opts] of _readerListeners) el.removeEventListener(type, fn, opts);
+ _readerListeners = [];
+}
+
function _readerDblClick(e) {
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
if (!e.target.closest('[data-epub-src], .pdf-page-wrapper')) return;
@@ -107,8 +123,10 @@ function cancelBookOpening() {
if (overlay) overlay.style.display = 'none';
const contentEl = $('reader-content');
if (contentEl) contentEl.innerHTML = '';
+ if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} }
currentPdfDoc = null;
currentPdfBuffer = null;
+ _readerUnlistenAll();
_readPos = null;
return true;
}
@@ -194,14 +212,17 @@ async function _openBookOnce(bookId) {
const isPdfBook = cachedMeta.type === 'pdf';
if (isPdfBook) {
- currentPdfDoc = null; // reset so renderPdf creates fresh doc
+ // A document still open (another book) goes; renderPdf loads this one.
+ if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} }
+ currentPdfDoc = null;
const result = await renderPdf(plain, contentEl);
if (left() || !result) return;
title = result.title || title;
author = result.author || author;
toc = result.toc;
numPages = result.numPages;
- currentPdfBuffer = plain;
+ // pdf.js holds the document: the decrypted bytes needn't stay as well.
+ currentPdfBuffer = null;
} else {
currentPdfBuffer = null;
// Prepared before (same book file, same preparation code): only the images to unpack.
@@ -270,10 +291,10 @@ async function _openBookOnce(bookId) {
}
// Touch: swipe (paginated) + pinch-to-zoom
- contentEl.addEventListener('touchstart', _pdfTouchStart, {passive: true});
- contentEl.addEventListener('touchmove', _pdfTouchMove, {passive: false});
- contentEl.addEventListener('touchend', _pdfTouchEnd, {passive: true});
- contentEl.addEventListener('touchcancel', _pdfTouchEnd, {passive: true});
+ _readerListen(contentEl, 'touchstart', _pdfTouchStart, {passive: true});
+ _readerListen(contentEl, 'touchmove', _pdfTouchMove, {passive: false});
+ _readerListen(contentEl, 'touchend', _pdfTouchEnd, {passive: true});
+ _readerListen(contentEl, 'touchcancel', _pdfTouchEnd, {passive: true});
// Set up progress input
const progressInput = $('reader-progress-input');
@@ -290,16 +311,12 @@ async function _openBookOnce(bookId) {
if (progressSuffix) progressSuffix.textContent = '%';
}
- progressInput.addEventListener('change', function () {
+ _readerListen(progressInput, 'change', function () {
_suppressScrollJumpDetect();
if (isPdf) {
const page = Math.min(numPages, Math.max(1, parseInt(this.value, 10) || 1));
this.value = page;
- const target = contentEl.querySelector(`#pdf-page-${page}`);
- if (target) {
- const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
- contentEl.scrollBy({top: top - 8, behavior: 'smooth'});
- }
+ pdfGoToPage(page);
} else {
const pct = Math.min(100, Math.max(0, _parseLocalePercent(this.value)));
this.value = _formatBookPercent(pct / 100);
@@ -307,7 +324,7 @@ async function _openBookOnce(bookId) {
}
});
- progressInput.addEventListener('click', function () { this.select(); });
+ _readerListen(progressInput, 'click', function () { this.select(); });
}
// Restore scroll position — must happen BEFORE auto-save timer is started
@@ -341,6 +358,7 @@ async function _openBookOnce(bookId) {
await new Promise(r => requestAnimationFrame(r));
if (fraction > 0)
contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight);
+ _pdfSetCurrentPage(_pdfTopPage(contentEl));
} else {
// EPUB — wait for images so scrollHeight is final
const imgs = Array.from(contentEl.querySelectorAll('img'));
@@ -373,29 +391,24 @@ async function _openBookOnce(bookId) {
// first — that could otherwise be a later, unrelated layout correction.
_scrollRestTop = contentEl.scrollTop;
- // Update progress input on scroll
- contentEl.addEventListener('scroll', () => {
- if (!progressInput) return;
- if (isPdf) {
- const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
- const cTop = contentEl.getBoundingClientRect().top;
- let currentPage = 1;
- for (const w of wrappers) {
- if (w.getBoundingClientRect().bottom > cTop + 20) {
- currentPage = parseInt(w.id.replace('pdf-page-', ''), 10) || 1;
- break;
- }
- }
- progressInput.value = currentPage;
- }
- // EPUB: shown from the position anchor, see the anchor tracking below.
- });
+ // PDF scrolling: the page shown at the top is the current one (header, bookmarks, the
+ // app's footer). EPUB: shown from the position anchor, see the anchor tracking below.
+ if (isPdf) {
+ let pageRaf = 0;
+ _readerListen(contentEl, 'scroll', () => {
+ if (pageRaf || readerSettings.pdfPaginated) return;
+ pageRaf = requestAnimationFrame(() => {
+ pageRaf = 0;
+ if (currentPdfDoc) _pdfSetCurrentPage(_pdfTopPage(contentEl));
+ });
+ }, {passive: true});
+ }
// Auto-save progress every 10s and on scroll (debounced 2s)
// Started AFTER restore so an early visibilitychange can't overwrite with position 0
readerScrollSaveTimer = setInterval(saveReaderProgress, 10000);
let _scrollDebounce = null;
- contentEl.addEventListener('scroll', () => {
+ _readerListen(contentEl, 'scroll', () => {
clearTimeout(_scrollDebounce);
_scrollDebounce = setTimeout(saveReaderProgress, 2000);
}, {passive: true});
@@ -405,7 +418,7 @@ async function _openBookOnce(bookId) {
// scrolling) would otherwise restore to a stale, pre-scroll anchor.
if (!isPdf) {
let _anchorTrackDebounce = null;
- contentEl.addEventListener('scroll', () => {
+ _readerListen(contentEl, 'scroll', () => {
clearTimeout(_anchorTrackDebounce);
_anchorTrackDebounce = setTimeout(() => {
_currentPositionAnchor = getPositionAnchor(contentEl);
@@ -434,7 +447,7 @@ async function _openBookOnce(bookId) {
// fat-fingered 'g'/'G' keybind, etc.) — see _checkScrollJump above.
_scrollRestTop = null;
let _scrollJumpRaf = null;
- contentEl.addEventListener('scroll', () => {
+ _readerListen(contentEl, 'scroll', () => {
if (_scrollJumpRaf) return;
_scrollJumpRaf = requestAnimationFrame(() => {
_scrollJumpRaf = null;
@@ -643,12 +656,12 @@ function closeReader() {
// Remove touch handlers and free image blob URLs
const contentEl = $('reader-content');
+ _readerUnlistenAll();
if (contentEl) {
- contentEl.removeEventListener('touchstart', _pdfTouchStart);
- contentEl.removeEventListener('touchmove', _pdfTouchMove);
- contentEl.removeEventListener('touchend', _pdfTouchEnd);
- contentEl.removeEventListener('touchcancel', _pdfTouchEnd);
- contentEl.classList.remove('pinch-active');
+ contentEl.removeEventListener('click', _pdfPaginatedClick);
+ contentEl.removeEventListener('wheel', _pdfPaginatedWheel);
+ contentEl.classList.remove('pinch-active', 'pdf-paginated');
+ contentEl.style.overflow = '';
contentEl.innerHTML = '';
}
for (const url of Object.values(currentImageMap)) URL.revokeObjectURL(url);
@@ -712,13 +725,9 @@ function jumpToTocEntry(href) {
const contentEl = $('reader-content');
if (!contentEl) return;
- // PDF page jump
+ // PDF page jump (scrolling or page by page)
if (href.startsWith('#pdf-page-')) {
- const target = contentEl.querySelector(href);
- if (target) {
- const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
- contentEl.scrollBy({top: top - 16, behavior: 'smooth'});
- }
+ pdfGoToPage(parseInt(href.slice('#pdf-page-'.length), 10) || 1);
return;
}
diff --git a/static/js/app/64-reader-settings.js b/static/js/app/64-reader-settings.js
index f50111e..328ffdb 100644
--- a/static/js/app/64-reader-settings.js
+++ b/static/js/app/64-reader-settings.js
@@ -173,8 +173,10 @@ function toggleSettingsPanel() {
} else {
panel.innerHTML = `
+
+
-
+
`;
}
@@ -312,7 +314,15 @@ function toggleSettingsPanel() {
} else {
const zoomRange = panel.querySelector('#rs-zoom');
- zoomRange.addEventListener('input', () => applyPdfZoom(parseInt(zoomRange.value, 10)));
+ // While the slider moves only the number follows; the pages are drawn anew once it rests.
+ let zoomTimer = 0;
+ zoomRange.addEventListener('input', () => {
+ const z = parseInt(zoomRange.value, 10);
+ const val = panel.querySelector('#rs-zoom-val');
+ if (val) val.textContent = z + '%';
+ clearTimeout(zoomTimer);
+ zoomTimer = setTimeout(() => applyPdfZoom(z), 250);
+ });
panel.querySelector('#rs-zoom-minus').addEventListener('click', () =>
applyPdfZoom(Math.max(50, readerSettings.pdfZoom - 10)));
panel.querySelector('#rs-zoom-plus').addEventListener('click', () =>
@@ -325,6 +335,29 @@ function toggleSettingsPanel() {
saveReaderSettings();
});
+ // Scrolling or page by page (phones and e-readers start with pages).
+ panel.querySelectorAll('[data-rs-pdfmode]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const pages = btn.dataset.rsPdfmode === 'pages';
+ if (pages === !!readerSettings.pdfPaginated || !currentPdfDoc) return;
+ const contentEl = $('reader-content');
+ const page = pages ? _pdfTopPage(contentEl) : pdfCurrentPage;
+ readerSettings.pdfPaginated = pages;
+ saveReaderSettings();
+ panel.querySelectorAll('[data-rs-pdfmode]').forEach(b => b.classList.toggle('active', b === btn));
+ const spread = panel.querySelector('#rs-spread');
+ if (spread) spread.style.display = pages ? 'none' : '';
+ if (pages) {
+ pdfCurrentPage = page;
+ // Spreads are a scrolling thing: one page at a time here.
+ if (readerSettings.pdfSpread) reRenderPdf(page).then(() => enterPdfPaginatedMode());
+ else enterPdfPaginatedMode();
+ } else {
+ exitPdfPaginatedMode(page);
+ }
+ });
+ });
+
panel.querySelector('#rs-spread').addEventListener('click', function () {
readerSettings.pdfSpread = !readerSettings.pdfSpread;
this.classList.toggle('active', readerSettings.pdfSpread);
@@ -349,23 +382,17 @@ async function applyPdfZoom(newZoom) {
}
}
+// The page at the top of the view (scrolling) — 58-pdf.js keeps the pages in order.
function _currentScrollPage(contentEl) {
- const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
- const viewTop = contentEl.scrollTop;
- for (const w of wrappers) {
- if (w.offsetTop + w.offsetHeight > viewTop) {
- const n = parseInt(w.id.replace('pdf-page-', ''), 10);
- if (n) return n;
- }
- }
- return pdfCurrentPage || 1;
+ return _pdfTopPage(contentEl);
}
-async function reRenderPdf() {
- if (!currentPdfBuffer) return;
+/** Lay the open PDF out again (zoom, spreads, another width), at [pivot] or the page shown now. */
+async function reRenderPdf(pivot) {
+ if (!currentPdfDoc) return;
const contentEl = $('reader-content');
if (!contentEl) return;
- const pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _currentScrollPage(contentEl);
- await renderPdf(currentPdfBuffer, contentEl, undefined, pivot);
+ if (!pivot) pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _pdfTopPage(contentEl);
+ await renderPdf(null, contentEl, undefined, pivot);
if (readerSettings.pdfPaginated) enterPdfPaginatedMode();
}
diff --git a/static/js/app/66-pdf-paginated.js b/static/js/app/66-pdf-paginated.js
index acfd020..57744b1 100644
--- a/static/js/app/66-pdf-paginated.js
+++ b/static/js/app/66-pdf-paginated.js
@@ -1,41 +1,73 @@
'use strict';
// ---------------------------------------------------------------------------
-// PDF Paginated Mode
+// PDF page by page (phones and e-readers by default; "Seiten" in the PDF panel):
+// one page at a time, cropped to its text so the letters are as large as the
+// screen allows, the next one drawn ahead for the turn. A tap on the left or
+// right turns (the middle shows the bars), so do a swipe, the arrow keys,
+// Space, PageUp/PageDown and the mouse wheel. Every page goes through
+// _pdfDraw (58-pdf.js).
// ---------------------------------------------------------------------------
+// A page nearly empty (only its number) would be zoomed to fill the screen with one digit:
+// at most this many times the size the whole page would have.
+const PDF_CROP_MAX = 2.5;
+const PDF_CROP_PAD = 12;
+
+let _pdfTurnSaveTimer = 0;
+let _pdfWheelSum = 0;
+let _pdfWheelAt = 0;
+let _touchStartY = 0;
+
function enterPdfPaginatedMode() {
const contentEl = $('reader-content');
if (!contentEl) return;
contentEl.classList.add('pdf-paginated');
contentEl.style.overflow = 'hidden';
const pdfVp = document.getElementById('pdf-viewport');
- if (pdfVp) pdfVp.style.zoom = 1;
-
- const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
- wrappers.forEach((w, i) => {
- w.style.display = (i + 1 === pdfCurrentPage) ? '' : 'none';
- });
-
- pdfSmartZoomPage(pdfCurrentPage);
-
- // Tap left/right to navigate
+ if (pdfVp) pdfVp.style.zoom = '';
+ // From scrolling: the drawing near the view stops, the pages are this mode's now.
+ const v = _pdfView;
+ if (v && v.observer) { v.observer.disconnect(); v.observer = null; v.queue.clear(); }
contentEl.addEventListener('click', _pdfPaginatedClick);
+ contentEl.addEventListener('wheel', _pdfPaginatedWheel, {passive: false});
+ _pdfShowPage(pdfCurrentPage);
}
-function exitPdfPaginatedMode() {
+/** Back to scrolling, at [page] (the one shown now). */
+function exitPdfPaginatedMode(page) {
const contentEl = $('reader-content');
if (!contentEl) return;
contentEl.classList.remove('pdf-paginated');
contentEl.style.overflow = '';
contentEl.removeEventListener('click', _pdfPaginatedClick);
- reRenderPdf();
+ contentEl.removeEventListener('wheel', _pdfPaginatedWheel);
+ reRenderPdf(page || pdfCurrentPage);
}
function _pdfPaginatedClick(e) {
- const w = e.currentTarget.clientWidth;
- if (e.clientX < w * 0.4) pdfGoToPage(pdfCurrentPage - 1);
- else if (e.clientX > w * 0.6) pdfGoToPage(pdfCurrentPage + 1);
+ if (e.target.closest('a, button, input, .pdf-text-layer span')) return;
+ const r = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - r.left;
+ const dir = x < r.width * 0.4 ? -1 : x > r.width * 0.6 ? 1 : 0;
+ if (!dir) return; // the middle: the bars come or go (62-reader.js)
+ // A turn is a turn only: not also the bars.
+ e.stopPropagation();
+ pdfGoToPage(pdfCurrentPage + dir);
+}
+
+function _pdfPaginatedWheel(e) {
+ if (e.ctrlKey) return; // the browser's zoom
+ e.preventDefault();
+ const now = Date.now();
+ if (now - _pdfWheelAt > 400) _pdfWheelSum = 0;
+ _pdfWheelAt = now;
+ _pdfWheelSum += e.deltaY;
+ if (Math.abs(_pdfWheelSum) < 60) return;
+ const dir = _pdfWheelSum > 0 ? 1 : -1;
+ _pdfWheelSum = 0;
+ _pdfWheelAt = now + 250; // one turn per flick of the wheel
+ pdfGoToPage(pdfCurrentPage + dir);
}
function _pdfTouchStart(e) {
@@ -49,6 +81,7 @@ function _pdfTouchStart(e) {
if (ce) ce.classList.add('pinch-active');
} else {
_touchStartX = e.touches[0].clientX;
+ _touchStartY = e.touches[0].clientY;
_isPinching = false;
}
}
@@ -71,105 +104,146 @@ function _pdfTouchEnd(e) {
const ce = $('reader-content');
if (ce) ce.classList.remove('pinch-active');
const vp = document.getElementById('pdf-viewport');
- const liveZoom = vp ? parseFloat(vp.style.zoom) * 100 : readerSettings.pdfZoom;
+ const liveZoom = vp && vp.style.zoom ? parseFloat(vp.style.zoom) * 100 : readerSettings.pdfZoom;
+ // The live zoom was only a preview: the page is drawn anew at the new size.
+ if (vp) vp.style.zoom = '';
const snapped = Math.max(50, Math.min(200, Math.round(liveZoom / 10) * 10));
applyPdfZoom(snapped);
return;
}
- if (!readerSettings.pdfPaginated) return;
- const delta = e.changedTouches[0].clientX - _touchStartX;
- if (delta > 50) pdfGoToPage(pdfCurrentPage - 1);
- else if (delta < -50) pdfGoToPage(pdfCurrentPage + 1);
+ if (!readerSettings.pdfPaginated || !e.changedTouches.length) return;
+ const dx = e.changedTouches[0].clientX - _touchStartX;
+ const dy = e.changedTouches[0].clientY - _touchStartY;
+ // Sideways, clearly: a slanted scroll is not a turn.
+ if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy) * 1.5) return;
+ pdfGoToPage(pdfCurrentPage + (dx > 0 ? -1 : 1));
}
+/** The page number the reader shows (header input) follows the page. */
+function _pdfSetCurrentPage(n) {
+ pdfCurrentPage = n;
+ const input = $('reader-progress-input');
+ if (input && currentPdfDoc && document.activeElement !== input) input.value = n;
+}
+
+/**
+ * Go to page [n], scrolling or page by page: the table of contents, the page
+ * number in the header, bookmarks, keys and the app's "Zu Seite springen".
+ */
function pdfGoToPage(n) {
if (!currentPdfDoc) return;
- n = Math.max(1, Math.min(pdfTotalPages, n));
- if (n === pdfCurrentPage) return;
+ n = Math.max(1, Math.min(pdfTotalPages, n | 0));
const contentEl = $('reader-content');
if (!contentEl) return;
-
- const oldWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`);
- if (oldWrapper) oldWrapper.style.display = 'none';
-
- pdfCurrentPage = n;
- const newWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`);
- if (newWrapper) newWrapper.style.display = '';
-
- pdfSmartZoomPage(pdfCurrentPage);
-
- const progressInput = $('reader-progress-input');
- if (progressInput) progressInput.value = pdfCurrentPage;
+ if (!readerSettings.pdfPaginated) {
+ const target = contentEl.querySelector(`#pdf-page-${n}`);
+ if (target) {
+ if (typeof _suppressScrollJumpDetect === 'function') _suppressScrollJumpDetect();
+ contentEl.scrollTop += target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top - 8;
+ }
+ _pdfSetCurrentPage(n);
+ return;
+ }
+ if (n === pdfCurrentPage && _pdfView && _pdfView.shown === n) return;
+ _pdfShowPage(n);
+ _pdfTurned();
}
-async function pdfSmartZoomPage(pageNum) {
- if (!currentPdfDoc) return;
- const contentEl = $('reader-content');
- if (!contentEl) return;
- const wrapper = contentEl.querySelector(`#pdf-page-${pageNum}`);
- if (!wrapper) return;
- const canvas = wrapper.querySelector('canvas');
- if (!canvas) return;
-
- const page = await currentPdfDoc.getPage(pageNum);
- const naturalVp = page.getViewport({scale: 1});
- const pageW = naturalVp.width;
- const pageH = naturalVp.height;
-
- let bbox = _pdfPageTextBoxCache[pageNum];
- if (!bbox) {
- bbox = await _computePdfTextBox(page, pageW, pageH);
- _pdfPageTextBoxCache[pageNum] = bbox;
- }
-
- const containerW = contentEl.clientWidth;
- const containerH = contentEl.clientHeight;
- const contentW = bbox.x2 - bbox.x1;
- const contentH = bbox.y2 - bbox.y1;
- const pad = 12;
- const scale = Math.min(
- (containerW - pad * 2) / contentW,
- (containerH - pad * 2) / contentH
- ) * (readerSettings.pdfZoom / 100);
-
- // Re-render canvas at new scale if significantly different
- const currentScale = canvas.width / naturalVp.width;
- if (Math.abs(scale - currentScale) / currentScale > 0.05) {
- const vp = page.getViewport({scale});
- canvas.width = vp.width;
- canvas.height = vp.height;
- await page.render({canvasContext: canvas.getContext('2d'), viewport: vp}).promise;
- }
-
- // Position canvas to center the text bounding box
- const renderedScale = canvas.width / naturalVp.width;
- const offsetX = -renderedScale * (bbox.x1 - pad) + (containerW - renderedScale * contentW - pad * 2) / 2;
- // PDF y-axis is bottom-up; canvas is top-down
- const offsetY = -renderedScale * (pageH - bbox.y2 - pad) + (containerH - renderedScale * contentH - pad * 2) / 2;
- canvas.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
- wrapper.style.overflow = 'hidden';
- wrapper.style.width = containerW + 'px';
- wrapper.style.height = containerH + 'px';
+// A turn is reading: the place is saved soon (not only by the 10 s timer), and e-readers
+// get their refresh (native-eink.js listens).
+function _pdfTurned() {
+ if (typeof _noteReaderInteraction === 'function') _noteReaderInteraction();
+ clearTimeout(_pdfTurnSaveTimer);
+ _pdfTurnSaveTimer = setTimeout(() => { if (currentPdfDoc) saveReaderProgress(); }, 1500);
+ try { window.dispatchEvent(new CustomEvent('diora-pdf-turn', {detail: {page: pdfCurrentPage}})); } catch (e) {}
}
-async function _computePdfTextBox(page, pageW, pageH) {
+/** Show page [n] alone, cropped; draw the next one ahead and let those far away go. */
+async function _pdfShowPage(n) {
+ const v = _pdfView;
+ if (!v || !v.entries.length) return;
+ n = Math.max(1, Math.min(v.entries.length, n));
+ _pdfSetCurrentPage(n);
+ v.shown = n;
+ const token = ++v.showToken;
+ v.entries.forEach(e => { e.wrapper.style.display = e.num === n ? '' : 'none'; });
+ try {
+ await _pdfFitPage(v, v.entries[n - 1]);
+ if (token !== v.showToken || v.stopped) return;
+ if (n < v.entries.length) await _pdfFitPage(v, v.entries[n], true);
+ if (token !== v.showToken || v.stopped) return;
+ if (n > 1 && !v.entries[n - 2].drawn) await _pdfFitPage(v, v.entries[n - 2], true);
+ } catch (err) {
+ console.warn('PDF page', n, err);
+ }
+ if (token !== v.showToken || v.stopped) return;
+ v.entries.forEach(e => { if (Math.abs(e.num - n) > 2 && (e.drawn || e.task)) _pdfLetGo(e, v.pdf); });
+}
+
+/**
+ * Page [e] sized to the screen by its text (its ink, for scans): drawn when the size differs,
+ * then placed so the text is centred. [ahead]: for the next turn, not shown yet.
+ */
+async function _pdfFitPage(v, e, ahead) {
+ const contentEl = v.contentEl;
+ const page = await v.pdf.getPage(e.num);
+ if (v.stopped) return;
+ const natural = page.getViewport({scale: 1});
+ let box = _pdfPageTextBoxCache[e.num];
+ if (!box) {
+ box = await _computePdfTextBox(page, natural);
+ _pdfPageTextBoxCache[e.num] = box;
+ }
+ if (v.stopped) return;
+ const W = contentEl.clientWidth;
+ const H = contentEl.clientHeight;
+ const bw = Math.max(1, box.x2 - box.x1);
+ const bh = Math.max(1, box.y2 - box.y1);
+ const fitBox = Math.min((W - PDF_CROP_PAD * 2) / bw, (H - PDF_CROP_PAD * 2) / bh);
+ const fitPage = Math.min((W - PDF_CROP_PAD * 2) / natural.width, (H - PDF_CROP_PAD * 2) / natural.height);
+ const scale = Math.max(0.1, Math.min(fitBox, fitPage * PDF_CROP_MAX)) * (readerSettings.pdfZoom / 100);
+ if (!e.drawn || Math.abs(scale - e.drawn) / e.drawn > 0.05) {
+ if (!await _pdfDraw(e, page, scale)) return;
+ }
+ // Centre the text box (at the scale it was drawn with).
+ const s = e.drawn;
+ const offsetX = (W - s * bw) / 2 - s * box.x1;
+ const offsetY = (H - s * bh) / 2 - s * box.y1;
+ e.inner.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
+ e.inner.style.transformOrigin = '0 0';
+ e.wrapper.style.overflow = 'hidden';
+ e.wrapper.style.width = W + 'px';
+ e.wrapper.style.height = H + 'px';
+ if (!ahead && v.shown === e.num) e.wrapper.style.display = '';
+}
+
+/**
+ * Where the page's content is, in the page's own top-down coordinates at scale 1 (as
+ * getViewport gives them, so turned pages and odd media boxes come out right): its text,
+ * or — a scan without text — what isn't white on a small drawing of it.
+ */
+async function _computePdfTextBox(page, natural) {
+ const pageW = natural.width, pageH = natural.height;
// Tier 1: text-based
try {
const tc = await page.getTextContent();
if (tc.items && tc.items.length) {
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
for (const item of tc.items) {
- if (!item.transform) continue;
+ if (!item.transform || !item.str || !item.str.trim()) continue;
const tx = item.transform[4], ty = item.transform[5];
- const iw = item.width || 0, ih = item.height || 0;
- if (tx < x1) x1 = tx;
- if (ty < y1) y1 = ty;
- if (tx + iw > x2) x2 = tx + iw;
- if (ty + ih > y2) y2 = ty + ih;
+ const iw = item.width || 0, ih = item.height || Math.abs(item.transform[3]) || 0;
+ for (const [px, py] of [[tx, ty], [tx + iw, ty + ih]]) {
+ const [vx, vy] = natural.convertToViewportPoint(px, py);
+ if (vx < x1) x1 = vx;
+ if (vx > x2) x2 = vx;
+ if (vy < y1) y1 = vy;
+ if (vy > y2) y2 = vy;
+ }
}
const area = (x2 - x1) * (y2 - y1);
if (isFinite(x1) && area > pageW * pageH * 0.25) {
- return {x1, y1, x2, y2};
+ return {x1: Math.max(0, x1), y1: Math.max(0, y1), x2: Math.min(pageW, x2), y2: Math.min(pageH, y2)};
}
}
} catch (e) {}
@@ -179,17 +253,18 @@ async function _computePdfTextBox(page, pageW, pageH) {
const lowScale = 0.3;
const vp = page.getViewport({scale: lowScale});
const offCanvas = document.createElement('canvas');
- offCanvas.width = vp.width;
- offCanvas.height = vp.height;
+ offCanvas.width = Math.ceil(vp.width);
+ offCanvas.height = Math.ceil(vp.height);
const ctx = offCanvas.getContext('2d');
await page.render({canvasContext: ctx, viewport: vp}).promise;
- const {data, width, height} = ctx.getImageData(0, 0, vp.width, vp.height);
+ const {data, width, height} = ctx.getImageData(0, 0, offCanvas.width, offCanvas.height);
+ offCanvas.width = offCanvas.height = 0;
let rMin = height, rMax = 0, cMin = width, cMax = 0;
for (let r = 0; r < height; r++) {
for (let c = 0; c < width; c++) {
const idx = (r * width + c) * 4;
- if (data[idx] + data[idx+1] + data[idx+2] < 720) {
+ if (data[idx] + data[idx + 1] + data[idx + 2] < 720) {
if (r < rMin) rMin = r;
if (r > rMax) rMax = r;
if (c < cMin) cMin = c;
@@ -198,12 +273,7 @@ async function _computePdfTextBox(page, pageW, pageH) {
}
}
if (rMin < rMax && cMin < cMax) {
- return {
- x1: cMin / lowScale,
- y1: (height - rMax) / lowScale,
- x2: cMax / lowScale,
- y2: (height - rMin) / lowScale,
- };
+ return {x1: cMin / lowScale, y1: rMin / lowScale, x2: cMax / lowScale, y2: rMax / lowScale};
}
} catch (e) {}
diff --git a/static/js/app/68-bookmarks.js b/static/js/app/68-bookmarks.js
index 783b369..8fdd94b 100644
--- a/static/js/app/68-bookmarks.js
+++ b/static/js/app/68-bookmarks.js
@@ -143,15 +143,7 @@ function jumpToBookmark(id) {
const contentEl = $('reader-content');
if (!contentEl) return;
if (bm.anchor.startsWith('pdf-page-')) {
- if (readerSettings.pdfPaginated) {
- pdfGoToPage(parseInt(bm.anchor.replace('pdf-page-', ''), 10) || 1);
- } else {
- const target = contentEl.querySelector('#' + bm.anchor);
- if (target) {
- const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
- contentEl.scrollBy({top: top - 16, behavior: 'smooth'});
- }
- }
+ pdfGoToPage(parseInt(bm.anchor.replace('pdf-page-', ''), 10) || 1);
} else {
const target = Array.from(contentEl.querySelectorAll('[data-epub-src]'))
.find(el => el.getAttribute('data-epub-src') === bm.anchor);
diff --git a/static/js/covers.js b/static/js/covers.js
index d1e02c8..d7ca566 100644
--- a/static/js/covers.js
+++ b/static/js/covers.js
@@ -122,16 +122,20 @@
async function pdfCover(bytes) {
if (typeof pdfjsLib === 'undefined') return null;
- const pdf = await pdfjsLib.getDocument({data: new Uint8Array(bytes)}).promise;
- const page = await pdf.getPage(1);
- const v = page.getViewport({scale: 1});
- const scale = Math.min(MAX_W / v.width, MAX_H / v.height);
- const viewport = page.getViewport({scale});
+ const pdf = await (typeof _pdfLoad === 'function' ? _pdfLoad(new Uint8Array(bytes))
+ : pdfjsLib.getDocument({data: new Uint8Array(bytes)})).promise;
const canvas = document.createElement('canvas');
- canvas.width = Math.round(viewport.width);
- canvas.height = Math.round(viewport.height);
- await page.render({canvasContext: canvas.getContext('2d'), viewport}).promise;
- pdf.destroy();
+ try {
+ const page = await pdf.getPage(1);
+ const v = page.getViewport({scale: 1});
+ const scale = Math.min(MAX_W / v.width, MAX_H / v.height);
+ const viewport = page.getViewport({scale});
+ canvas.width = Math.round(viewport.width);
+ canvas.height = Math.round(viewport.height);
+ await page.render({canvasContext: canvas.getContext('2d'), viewport}).promise;
+ } finally {
+ try { pdf.destroy(); } catch (e) {}
+ }
return new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', QUALITY));
}