Compare commits

...

4 commits

Author SHA1 Message Date
6a3cd23095 PDF-Reader überarbeitet: ein Zeichenweg, Seitenmodus nach Textbox, Seitensprung, Inhalt und Lesezeichen in beiden Modi (sw CACHE v164)
Some checks failed
Test / e2e (push) Waiting to run
Build and push Docker image / build (push) Successful in 17s
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:09:05 +01:00
ec704a086f PDF Seitenmodus: Seite füllt den Lesebereich und wird nach ihrer Textbox platziert
Die Leser-Regel „.reader-content > * { max-width: 65ch }“ verengte auch #pdf-viewport, im
Querformat saß die Seite deshalb seitlich versetzt; dazu kamen Polsterung und Flex-Zentrierung
oben drauf. Seitenweise misst 66-pdf-paginated.js den Bereich und setzt die Seite selbst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:07:18 +01:00
a963841bc4 Test-PDF als Binärdatei (keine Zeilenend-Umwandlung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:52:39 +01:00
c2a9873b0d 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
15 changed files with 1243 additions and 259 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.pdf binary

View file

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

641
e2e/fixtures/test.pdf Normal file
View file

@ -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

View file

@ -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; }
@ -1837,8 +1838,13 @@ html.nui-tts-on body.reader-immersive .now-playing-bar { transform: none; }
html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); }
/* PDF paginated */
.reader-content.pdf-paginated { overflow:hidden !important; display:flex; align-items:center; justify-content:center; }
/* Page by page the page fills the whole reading area, without its padding: 66-pdf-paginated.js
measures the area and places the page by its text box itself. */
.reader-content.pdf-paginated { overflow:hidden !important; display:block; padding:0 !important; }
.reader-content.pdf-paginated > #pdf-viewport { width:100%; height:100%; max-width:none; margin:0; }
.pdf-paginated .pdf-page-wrapper { margin:0; }
.pdf-paginated .pdf-page-wrapper { position:relative; }
.pdf-paginated .pdf-page-inner { position:absolute; left:0; top:0; }
/* PDF loading overlay */
.pdf-loading-overlay {

View file

@ -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

View file

@ -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');

View file

@ -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 {

View file

@ -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 {

View file

@ -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();
}

View file

@ -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;
}

View file

@ -173,8 +173,10 @@ function toggleSettingsPanel() {
} else {
panel.innerHTML = `
<label>Zoom <button class="btn btn-sm" id="rs-zoom-minus">−</button> <input type="range" id="rs-zoom" min="50" max="200" step="10" value="${readerSettings.pdfZoom}"> <button class="btn btn-sm" id="rs-zoom-plus">+</button> <span id="rs-zoom-val">${readerSettings.pdfZoom}%</span></label>
<button class="btn btn-sm ${readerSettings.pdfPaginated ? '' : 'active'}" data-rs-pdfmode="scroll">Scrollen</button>
<button class="btn btn-sm ${readerSettings.pdfPaginated ? 'active' : ''}" data-rs-pdfmode="pages">Seiten</button>
<button class="btn btn-sm ${readerSettings.pdfInverted ? 'active' : ''}" id="rs-invert">Invertieren</button>
<button class="btn btn-sm ${readerSettings.pdfSpread ? 'active' : ''}" id="rs-spread">Doppelseite</button>
<button class="btn btn-sm ${readerSettings.pdfSpread ? 'active' : ''}" id="rs-spread"${readerSettings.pdfPaginated ? ' style="display:none"' : ''}>Doppelseite</button>
`;
}
@ -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();
}

View file

@ -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) {}

View file

@ -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);

View file

@ -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));
}

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use.
*/
const CACHE = 'diora-v163';
const CACHE = 'diora-v164';
const SHELL = [
'/static/css/app.css',
'/static/js/app/01-core.js',