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>
This commit is contained in:
parent
04548171bf
commit
c2a9873b0d
13 changed files with 1235 additions and 257 deletions
118
e2e/fixtures/make_test_pdf.py
Normal file
118
e2e/fixtures/make_test_pdf.py
Normal 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
641
e2e/fixtures/test.pdf
Normal 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
|
||||||
|
|
@ -1812,7 +1812,8 @@ body.dnd-mode .timer-display {
|
||||||
.pdf-text-layer span.reader-search-match.active { background:rgba(230,57,70,.6); }
|
.pdf-text-layer span.reader-search-match.active { background:rgba(230,57,70,.6); }
|
||||||
|
|
||||||
/* PDF invert */
|
/* 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 ---- */
|
/* ---- Immersive reader mode ---- */
|
||||||
.navbar { transition: transform 0.35s ease; }
|
.navbar { transition: transform 0.35s ease; }
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,12 @@ document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'ArrowDown') { e.preventDefault(); readerSearchNext(); }
|
if (e.key === 'ArrowDown') { e.preventDefault(); readerSearchNext(); }
|
||||||
if (e.key === 'ArrowUp') { e.preventDefault(); readerSearchPrev(); }
|
if (e.key === 'ArrowUp') { e.preventDefault(); readerSearchPrev(); }
|
||||||
}
|
}
|
||||||
if (readerSettings.pdfPaginated && currentPdfDoc) {
|
// PDF page by page: arrows, Space, PageUp/PageDown turn (scrolling has its own keys below).
|
||||||
if (e.key === 'ArrowRight') { e.preventDefault(); pdfGoToPage(pdfCurrentPage + 1); }
|
if (readerSettings.pdfPaginated && currentPdfDoc && !readerSearchOpen
|
||||||
if (e.key === 'ArrowLeft') { e.preventDefault(); pdfGoToPage(pdfCurrentPage - 1); }
|
&& !/^(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
|
// Vim-style scroll — ignore when typing in an input
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// spine text by CHARS_PER_BOOK_PAGE (as the reader estimates it). The library's length sorts.
|
||||||
async function bookPagesOfFile(buf, type) {
|
async function bookPagesOfFile(buf, type) {
|
||||||
if (type === 'pdf') {
|
if (type === 'pdf') {
|
||||||
const pdf = await pdfjsLib.getDocument({data: new Uint8Array(buf)}).promise;
|
const pdf = await _pdfLoad(new Uint8Array(buf)).promise;
|
||||||
const n = pdf.numPages;
|
try {
|
||||||
pdf.destroy();
|
return pdf.numPages;
|
||||||
return n;
|
} finally {
|
||||||
|
try { pdf.destroy(); } catch (e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const zip = await JSZip.loadAsync(buf);
|
const zip = await JSZip.loadAsync(buf);
|
||||||
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,14 @@ function repairBook(bookId) {
|
||||||
let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = '', isbn = '';
|
let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = '', isbn = '';
|
||||||
try {
|
try {
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise;
|
const pdfDoc = await _pdfLoad(new Uint8Array(buf.slice(0))).promise;
|
||||||
|
try {
|
||||||
const meta = await pdfDoc.getMetadata();
|
const meta = await pdfDoc.getMetadata();
|
||||||
title = meta.info?.Title?.trim() || title;
|
title = meta.info?.Title?.trim() || title;
|
||||||
author = meta.info?.Author?.trim() || '';
|
author = meta.info?.Author?.trim() || '';
|
||||||
|
} finally {
|
||||||
|
try { pdfDoc.destroy(); } catch (e) {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const zip = await JSZip.loadAsync(buf.slice(0));
|
const zip = await JSZip.loadAsync(buf.slice(0));
|
||||||
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
||||||
|
|
@ -785,10 +789,14 @@ async function _importEbookBuffer(buf, filename, statusEl, opts) {
|
||||||
|
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise;
|
const pdfDoc = await _pdfLoad(new Uint8Array(buf.slice(0))).promise;
|
||||||
|
try {
|
||||||
const meta = await pdfDoc.getMetadata();
|
const meta = await pdfDoc.getMetadata();
|
||||||
title = meta.info?.Title?.trim() || title;
|
title = meta.info?.Title?.trim() || title;
|
||||||
author = meta.info?.Author?.trim() || '';
|
author = meta.info?.Author?.trim() || '';
|
||||||
|
} finally {
|
||||||
|
try { pdfDoc.destroy(); } catch (e) {}
|
||||||
|
}
|
||||||
} catch (e) { /* use filename as title */ }
|
} catch (e) { /* use filename as title */ }
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ const _issueNorm = s => String(s || '').toLowerCase().replace(/ß/g, 'ss').norma
|
||||||
|
|
||||||
async function _issueIndexPdf(bytes) {
|
async function _issueIndexPdf(bytes) {
|
||||||
if (typeof pdfjsLib === 'undefined') return null;
|
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 {
|
try {
|
||||||
return await _issuePdfArticles(pdf);
|
return await _issuePdfArticles(pdf);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,145 @@
|
||||||
'use strict';
|
'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) {
|
async function _parsePdfOutline(pdf, items, depth) {
|
||||||
depth = depth || 0;
|
depth = depth || 0;
|
||||||
const result = [];
|
const parts = await Promise.all(items.map(async item => {
|
||||||
for (const item of items) {
|
|
||||||
let href = '';
|
let href = '';
|
||||||
if (item.dest) {
|
if (item.dest) {
|
||||||
try {
|
try {
|
||||||
const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
|
const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
|
||||||
if (dest) {
|
if (dest && dest[0] != null) {
|
||||||
const pageIndex = await pdf.getPageIndex(dest[0]);
|
const index = typeof dest[0] === 'number' ? dest[0] : await pdf.getPageIndex(dest[0]);
|
||||||
href = `#pdf-page-${pageIndex + 1}`;
|
href = `#pdf-page-${index + 1}`;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
result.push({label: item.title || '(untitled)', href, depth});
|
const kids = item.items && item.items.length ? await _parsePdfOutline(pdf, item.items, depth + 1) : [];
|
||||||
if (item.items && item.items.length) {
|
return [{label: item.title || '(untitled)', href, depth}].concat(kids);
|
||||||
result.push(...await _parsePdfOutline(pdf, item.items, depth + 1));
|
}));
|
||||||
}
|
return [].concat.apply([], parts);
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The document being loaded (pdf.js loading task), so opening can be cancelled.
|
// The document being loaded (pdf.js loading task), so opening can be cancelled.
|
||||||
let _pdfLoadingTask = null;
|
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;
|
let _pdfLazy = null;
|
||||||
|
|
||||||
function _pdfStopLazy() {
|
function _pdfStopLazy() {
|
||||||
if (_pdfLazy) {
|
const v = _pdfView;
|
||||||
_pdfLazy.stopped = true;
|
if (!v) return;
|
||||||
if (_pdfLazy.observer) _pdfLazy.observer.disconnect();
|
v.stopped = true;
|
||||||
_pdfLazy = null;
|
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
|
// 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
|
// 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) {
|
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
||||||
const myGen = ++_pdfRenderGen;
|
const myGen = ++_pdfRenderGen;
|
||||||
_pdfStopLazy();
|
_pdfStopLazy();
|
||||||
let pdf = currentPdfDoc;
|
let pdf = currentPdfDoc;
|
||||||
if (!pdf) {
|
if (!pdf) {
|
||||||
_pdfLoadingTask = pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))});
|
_pdfLoadingTask = _pdfLoad(new Uint8Array(arrayBuffer.slice(0)));
|
||||||
try {
|
try {
|
||||||
pdf = await _pdfLoadingTask.promise;
|
pdf = await _pdfLoadingTask.promise;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -77,7 +173,6 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
||||||
? contentEl.clientWidth - 32
|
? contentEl.clientWidth - 32
|
||||||
: Math.min(contentEl.clientWidth - 32, 900);
|
: Math.min(contentEl.clientWidth - 32, 900);
|
||||||
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
|
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
|
||||||
const dpr = window.devicePixelRatio || 1;
|
|
||||||
const scaleFor = vp => (scaleOverride != null ? scaleOverride
|
const scaleFor = vp => (scaleOverride != null ? scaleOverride
|
||||||
: Math.max(0.5, pageWidth / vp.width) * (readerSettings.pdfZoom / 100));
|
: 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';
|
canvas.style.height = firstVp.height + 'px';
|
||||||
inner.appendChild(canvas);
|
inner.appendChild(canvas);
|
||||||
wrapper.appendChild(inner);
|
wrapper.appendChild(inner);
|
||||||
if (!readerSettings.pdfSpread) {
|
if (!readerSettings.pdfSpread || readerSettings.pdfPaginated) {
|
||||||
pdfVp.appendChild(wrapper);
|
pdfVp.appendChild(wrapper);
|
||||||
} else if (pageNum === 1) {
|
} else if (pageNum === 1) {
|
||||||
wrapper.classList.add('pdf-spread-cover');
|
wrapper.classList.add('pdf-spread-cover');
|
||||||
|
|
@ -117,78 +212,86 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
||||||
if (spreadContainer) spreadContainer.appendChild(wrapper);
|
if (spreadContainer) spreadContainer.appendChild(wrapper);
|
||||||
else pdfVp.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;
|
pdfTotalPages = pdf.numPages;
|
||||||
|
|
||||||
const pivot = Math.max(1, Math.min(pdf.numPages, pivotPage || 1));
|
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};
|
const view = {pdf, contentEl, entries, gen: myGen, stopped: false, observer: null, resize: null,
|
||||||
_pdfLazy = lazy;
|
resizeTimer: 0, queue: new Set(), working: false, scaleFor, showToken: 0};
|
||||||
const gone = () => lazy.stopped || _pdfRenderGen !== myGen;
|
_pdfView = _pdfLazy = view;
|
||||||
|
const gone = () => view.stopped || _pdfRenderGen !== myGen;
|
||||||
|
|
||||||
async function draw(e) {
|
async function draw(e) {
|
||||||
const page = await pdf.getPage(e.num);
|
const page = await pdf.getPage(e.num);
|
||||||
if (gone() || !e.near) return;
|
if (gone() || !e.near || readerSettings.pdfPaginated) return;
|
||||||
const viewport = page.getViewport({scale: scaleFor(page.getViewport({scale: 1}))});
|
await _pdfDraw(e, page, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One page at a time, the nearest to the current one first.
|
// One page at a time, the nearest to the current one first.
|
||||||
async function work() {
|
async function work() {
|
||||||
if (lazy.working) return;
|
if (view.working) return;
|
||||||
lazy.working = true;
|
view.working = true;
|
||||||
try {
|
try {
|
||||||
while (!gone() && lazy.queue.size) {
|
while (!gone() && view.queue.size && !readerSettings.pdfPaginated) {
|
||||||
const cur = typeof _currentScrollPage === 'function' && !readerSettings.pdfPaginated
|
const cur = _pdfTopPage(contentEl);
|
||||||
? _currentScrollPage(contentEl) : (pdfCurrentPage || pivot);
|
const next = Array.from(view.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
|
||||||
const next = Array.from(lazy.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
|
view.queue.delete(next);
|
||||||
lazy.queue.delete(next);
|
|
||||||
if (next.near && !next.drawn) await draw(next);
|
if (next.near && !next.drawn) await draw(next);
|
||||||
}
|
}
|
||||||
} finally {
|
} 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]));
|
const byEl = new Map(entries.map(e => [e.wrapper, e]));
|
||||||
if (typeof IntersectionObserver === 'function') {
|
if (typeof IntersectionObserver === 'function') {
|
||||||
lazy.observer = new IntersectionObserver(items => {
|
view.observer = new IntersectionObserver(items => {
|
||||||
|
if (readerSettings.pdfPaginated) return;
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
const e = byEl.get(it.target);
|
const e = byEl.get(it.target);
|
||||||
if (!e) continue;
|
if (!e) continue;
|
||||||
e.near = it.isIntersecting;
|
e.near = it.isIntersecting;
|
||||||
if (e.near) { if (!e.drawn) lazy.queue.add(e); }
|
if (e.near) { if (!e.drawn) view.queue.add(e); }
|
||||||
else { lazy.queue.delete(e); letGo(e); }
|
else { view.queue.delete(e); if (e.drawn || e.task) _pdfLetGo(e, pdf); }
|
||||||
}
|
}
|
||||||
work();
|
work();
|
||||||
}, {root: contentEl, rootMargin: '150% 0px'});
|
}, {root: contentEl, rootMargin: '150% 0px'});
|
||||||
entries.forEach(e => lazy.observer.observe(e.wrapper));
|
entries.forEach(e => view.observer.observe(e.wrapper));
|
||||||
} else {
|
} else {
|
||||||
// Very old WebViews: every page, from the pivot on (as before).
|
// 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();
|
work();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,22 @@ function _immHandleTap(e) {
|
||||||
// the text a click turns the page (63-pages.js), so only on the text itself.
|
// the text a click turns the page (63-pages.js), so only on the text itself.
|
||||||
let _readerFsHidBars = false;
|
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) {
|
function _readerDblClick(e) {
|
||||||
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
|
if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return;
|
||||||
if (!e.target.closest('[data-epub-src], .pdf-page-wrapper')) return;
|
if (!e.target.closest('[data-epub-src], .pdf-page-wrapper')) return;
|
||||||
|
|
@ -107,8 +123,10 @@ function cancelBookOpening() {
|
||||||
if (overlay) overlay.style.display = 'none';
|
if (overlay) overlay.style.display = 'none';
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (contentEl) contentEl.innerHTML = '';
|
if (contentEl) contentEl.innerHTML = '';
|
||||||
|
if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} }
|
||||||
currentPdfDoc = null;
|
currentPdfDoc = null;
|
||||||
currentPdfBuffer = null;
|
currentPdfBuffer = null;
|
||||||
|
_readerUnlistenAll();
|
||||||
_readPos = null;
|
_readPos = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -194,14 +212,17 @@ async function _openBookOnce(bookId) {
|
||||||
const isPdfBook = cachedMeta.type === 'pdf';
|
const isPdfBook = cachedMeta.type === 'pdf';
|
||||||
|
|
||||||
if (isPdfBook) {
|
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);
|
const result = await renderPdf(plain, contentEl);
|
||||||
if (left() || !result) return;
|
if (left() || !result) return;
|
||||||
title = result.title || title;
|
title = result.title || title;
|
||||||
author = result.author || author;
|
author = result.author || author;
|
||||||
toc = result.toc;
|
toc = result.toc;
|
||||||
numPages = result.numPages;
|
numPages = result.numPages;
|
||||||
currentPdfBuffer = plain;
|
// pdf.js holds the document: the decrypted bytes needn't stay as well.
|
||||||
|
currentPdfBuffer = null;
|
||||||
} else {
|
} else {
|
||||||
currentPdfBuffer = null;
|
currentPdfBuffer = null;
|
||||||
// Prepared before (same book file, same preparation code): only the images to unpack.
|
// 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
|
// Touch: swipe (paginated) + pinch-to-zoom
|
||||||
contentEl.addEventListener('touchstart', _pdfTouchStart, {passive: true});
|
_readerListen(contentEl, 'touchstart', _pdfTouchStart, {passive: true});
|
||||||
contentEl.addEventListener('touchmove', _pdfTouchMove, {passive: false});
|
_readerListen(contentEl, 'touchmove', _pdfTouchMove, {passive: false});
|
||||||
contentEl.addEventListener('touchend', _pdfTouchEnd, {passive: true});
|
_readerListen(contentEl, 'touchend', _pdfTouchEnd, {passive: true});
|
||||||
contentEl.addEventListener('touchcancel', _pdfTouchEnd, {passive: true});
|
_readerListen(contentEl, 'touchcancel', _pdfTouchEnd, {passive: true});
|
||||||
|
|
||||||
// Set up progress input
|
// Set up progress input
|
||||||
const progressInput = $('reader-progress-input');
|
const progressInput = $('reader-progress-input');
|
||||||
|
|
@ -290,16 +311,12 @@ async function _openBookOnce(bookId) {
|
||||||
if (progressSuffix) progressSuffix.textContent = '%';
|
if (progressSuffix) progressSuffix.textContent = '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
progressInput.addEventListener('change', function () {
|
_readerListen(progressInput, 'change', function () {
|
||||||
_suppressScrollJumpDetect();
|
_suppressScrollJumpDetect();
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
const page = Math.min(numPages, Math.max(1, parseInt(this.value, 10) || 1));
|
const page = Math.min(numPages, Math.max(1, parseInt(this.value, 10) || 1));
|
||||||
this.value = page;
|
this.value = page;
|
||||||
const target = contentEl.querySelector(`#pdf-page-${page}`);
|
pdfGoToPage(page);
|
||||||
if (target) {
|
|
||||||
const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
|
|
||||||
contentEl.scrollBy({top: top - 8, behavior: 'smooth'});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const pct = Math.min(100, Math.max(0, _parseLocalePercent(this.value)));
|
const pct = Math.min(100, Math.max(0, _parseLocalePercent(this.value)));
|
||||||
this.value = _formatBookPercent(pct / 100);
|
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
|
// 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));
|
await new Promise(r => requestAnimationFrame(r));
|
||||||
if (fraction > 0)
|
if (fraction > 0)
|
||||||
contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight);
|
contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight);
|
||||||
|
_pdfSetCurrentPage(_pdfTopPage(contentEl));
|
||||||
} else {
|
} else {
|
||||||
// EPUB — wait for images so scrollHeight is final
|
// EPUB — wait for images so scrollHeight is final
|
||||||
const imgs = Array.from(contentEl.querySelectorAll('img'));
|
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.
|
// first — that could otherwise be a later, unrelated layout correction.
|
||||||
_scrollRestTop = contentEl.scrollTop;
|
_scrollRestTop = contentEl.scrollTop;
|
||||||
|
|
||||||
// Update progress input on scroll
|
// PDF scrolling: the page shown at the top is the current one (header, bookmarks, the
|
||||||
contentEl.addEventListener('scroll', () => {
|
// app's footer). EPUB: shown from the position anchor, see the anchor tracking below.
|
||||||
if (!progressInput) return;
|
|
||||||
if (isPdf) {
|
if (isPdf) {
|
||||||
const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
|
let pageRaf = 0;
|
||||||
const cTop = contentEl.getBoundingClientRect().top;
|
_readerListen(contentEl, 'scroll', () => {
|
||||||
let currentPage = 1;
|
if (pageRaf || readerSettings.pdfPaginated) return;
|
||||||
for (const w of wrappers) {
|
pageRaf = requestAnimationFrame(() => {
|
||||||
if (w.getBoundingClientRect().bottom > cTop + 20) {
|
pageRaf = 0;
|
||||||
currentPage = parseInt(w.id.replace('pdf-page-', ''), 10) || 1;
|
if (currentPdfDoc) _pdfSetCurrentPage(_pdfTopPage(contentEl));
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progressInput.value = currentPage;
|
|
||||||
}
|
|
||||||
// EPUB: shown from the position anchor, see the anchor tracking below.
|
|
||||||
});
|
});
|
||||||
|
}, {passive: true});
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-save progress every 10s and on scroll (debounced 2s)
|
// Auto-save progress every 10s and on scroll (debounced 2s)
|
||||||
// Started AFTER restore so an early visibilitychange can't overwrite with position 0
|
// Started AFTER restore so an early visibilitychange can't overwrite with position 0
|
||||||
readerScrollSaveTimer = setInterval(saveReaderProgress, 10000);
|
readerScrollSaveTimer = setInterval(saveReaderProgress, 10000);
|
||||||
let _scrollDebounce = null;
|
let _scrollDebounce = null;
|
||||||
contentEl.addEventListener('scroll', () => {
|
_readerListen(contentEl, 'scroll', () => {
|
||||||
clearTimeout(_scrollDebounce);
|
clearTimeout(_scrollDebounce);
|
||||||
_scrollDebounce = setTimeout(saveReaderProgress, 2000);
|
_scrollDebounce = setTimeout(saveReaderProgress, 2000);
|
||||||
}, {passive: true});
|
}, {passive: true});
|
||||||
|
|
@ -405,7 +418,7 @@ async function _openBookOnce(bookId) {
|
||||||
// scrolling) would otherwise restore to a stale, pre-scroll anchor.
|
// scrolling) would otherwise restore to a stale, pre-scroll anchor.
|
||||||
if (!isPdf) {
|
if (!isPdf) {
|
||||||
let _anchorTrackDebounce = null;
|
let _anchorTrackDebounce = null;
|
||||||
contentEl.addEventListener('scroll', () => {
|
_readerListen(contentEl, 'scroll', () => {
|
||||||
clearTimeout(_anchorTrackDebounce);
|
clearTimeout(_anchorTrackDebounce);
|
||||||
_anchorTrackDebounce = setTimeout(() => {
|
_anchorTrackDebounce = setTimeout(() => {
|
||||||
_currentPositionAnchor = getPositionAnchor(contentEl);
|
_currentPositionAnchor = getPositionAnchor(contentEl);
|
||||||
|
|
@ -434,7 +447,7 @@ async function _openBookOnce(bookId) {
|
||||||
// fat-fingered 'g'/'G' keybind, etc.) — see _checkScrollJump above.
|
// fat-fingered 'g'/'G' keybind, etc.) — see _checkScrollJump above.
|
||||||
_scrollRestTop = null;
|
_scrollRestTop = null;
|
||||||
let _scrollJumpRaf = null;
|
let _scrollJumpRaf = null;
|
||||||
contentEl.addEventListener('scroll', () => {
|
_readerListen(contentEl, 'scroll', () => {
|
||||||
if (_scrollJumpRaf) return;
|
if (_scrollJumpRaf) return;
|
||||||
_scrollJumpRaf = requestAnimationFrame(() => {
|
_scrollJumpRaf = requestAnimationFrame(() => {
|
||||||
_scrollJumpRaf = null;
|
_scrollJumpRaf = null;
|
||||||
|
|
@ -643,12 +656,12 @@ function closeReader() {
|
||||||
|
|
||||||
// Remove touch handlers and free image blob URLs
|
// Remove touch handlers and free image blob URLs
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
|
_readerUnlistenAll();
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
contentEl.removeEventListener('touchstart', _pdfTouchStart);
|
contentEl.removeEventListener('click', _pdfPaginatedClick);
|
||||||
contentEl.removeEventListener('touchmove', _pdfTouchMove);
|
contentEl.removeEventListener('wheel', _pdfPaginatedWheel);
|
||||||
contentEl.removeEventListener('touchend', _pdfTouchEnd);
|
contentEl.classList.remove('pinch-active', 'pdf-paginated');
|
||||||
contentEl.removeEventListener('touchcancel', _pdfTouchEnd);
|
contentEl.style.overflow = '';
|
||||||
contentEl.classList.remove('pinch-active');
|
|
||||||
contentEl.innerHTML = '';
|
contentEl.innerHTML = '';
|
||||||
}
|
}
|
||||||
for (const url of Object.values(currentImageMap)) URL.revokeObjectURL(url);
|
for (const url of Object.values(currentImageMap)) URL.revokeObjectURL(url);
|
||||||
|
|
@ -712,13 +725,9 @@ function jumpToTocEntry(href) {
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
|
|
||||||
// PDF page jump
|
// PDF page jump (scrolling or page by page)
|
||||||
if (href.startsWith('#pdf-page-')) {
|
if (href.startsWith('#pdf-page-')) {
|
||||||
const target = contentEl.querySelector(href);
|
pdfGoToPage(parseInt(href.slice('#pdf-page-'.length), 10) || 1);
|
||||||
if (target) {
|
|
||||||
const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
|
|
||||||
contentEl.scrollBy({top: top - 16, behavior: 'smooth'});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,10 @@ function toggleSettingsPanel() {
|
||||||
} else {
|
} else {
|
||||||
panel.innerHTML = `
|
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>
|
<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.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 {
|
} else {
|
||||||
const zoomRange = panel.querySelector('#rs-zoom');
|
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', () =>
|
panel.querySelector('#rs-zoom-minus').addEventListener('click', () =>
|
||||||
applyPdfZoom(Math.max(50, readerSettings.pdfZoom - 10)));
|
applyPdfZoom(Math.max(50, readerSettings.pdfZoom - 10)));
|
||||||
panel.querySelector('#rs-zoom-plus').addEventListener('click', () =>
|
panel.querySelector('#rs-zoom-plus').addEventListener('click', () =>
|
||||||
|
|
@ -325,6 +335,29 @@ function toggleSettingsPanel() {
|
||||||
saveReaderSettings();
|
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 () {
|
panel.querySelector('#rs-spread').addEventListener('click', function () {
|
||||||
readerSettings.pdfSpread = !readerSettings.pdfSpread;
|
readerSettings.pdfSpread = !readerSettings.pdfSpread;
|
||||||
this.classList.toggle('active', 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) {
|
function _currentScrollPage(contentEl) {
|
||||||
const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
|
return _pdfTopPage(contentEl);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reRenderPdf() {
|
/** Lay the open PDF out again (zoom, spreads, another width), at [pivot] or the page shown now. */
|
||||||
if (!currentPdfBuffer) return;
|
async function reRenderPdf(pivot) {
|
||||||
|
if (!currentPdfDoc) return;
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
const pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _currentScrollPage(contentEl);
|
if (!pivot) pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _pdfTopPage(contentEl);
|
||||||
await renderPdf(currentPdfBuffer, contentEl, undefined, pivot);
|
await renderPdf(null, contentEl, undefined, pivot);
|
||||||
if (readerSettings.pdfPaginated) enterPdfPaginatedMode();
|
if (readerSettings.pdfPaginated) enterPdfPaginatedMode();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,73 @@
|
||||||
'use strict';
|
'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() {
|
function enterPdfPaginatedMode() {
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
contentEl.classList.add('pdf-paginated');
|
contentEl.classList.add('pdf-paginated');
|
||||||
contentEl.style.overflow = 'hidden';
|
contentEl.style.overflow = 'hidden';
|
||||||
const pdfVp = document.getElementById('pdf-viewport');
|
const pdfVp = document.getElementById('pdf-viewport');
|
||||||
if (pdfVp) pdfVp.style.zoom = 1;
|
if (pdfVp) pdfVp.style.zoom = '';
|
||||||
|
// From scrolling: the drawing near the view stops, the pages are this mode's now.
|
||||||
const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
|
const v = _pdfView;
|
||||||
wrappers.forEach((w, i) => {
|
if (v && v.observer) { v.observer.disconnect(); v.observer = null; v.queue.clear(); }
|
||||||
w.style.display = (i + 1 === pdfCurrentPage) ? '' : 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
pdfSmartZoomPage(pdfCurrentPage);
|
|
||||||
|
|
||||||
// Tap left/right to navigate
|
|
||||||
contentEl.addEventListener('click', _pdfPaginatedClick);
|
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');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
contentEl.classList.remove('pdf-paginated');
|
contentEl.classList.remove('pdf-paginated');
|
||||||
contentEl.style.overflow = '';
|
contentEl.style.overflow = '';
|
||||||
contentEl.removeEventListener('click', _pdfPaginatedClick);
|
contentEl.removeEventListener('click', _pdfPaginatedClick);
|
||||||
reRenderPdf();
|
contentEl.removeEventListener('wheel', _pdfPaginatedWheel);
|
||||||
|
reRenderPdf(page || pdfCurrentPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _pdfPaginatedClick(e) {
|
function _pdfPaginatedClick(e) {
|
||||||
const w = e.currentTarget.clientWidth;
|
if (e.target.closest('a, button, input, .pdf-text-layer span')) return;
|
||||||
if (e.clientX < w * 0.4) pdfGoToPage(pdfCurrentPage - 1);
|
const r = e.currentTarget.getBoundingClientRect();
|
||||||
else if (e.clientX > w * 0.6) pdfGoToPage(pdfCurrentPage + 1);
|
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) {
|
function _pdfTouchStart(e) {
|
||||||
|
|
@ -49,6 +81,7 @@ function _pdfTouchStart(e) {
|
||||||
if (ce) ce.classList.add('pinch-active');
|
if (ce) ce.classList.add('pinch-active');
|
||||||
} else {
|
} else {
|
||||||
_touchStartX = e.touches[0].clientX;
|
_touchStartX = e.touches[0].clientX;
|
||||||
|
_touchStartY = e.touches[0].clientY;
|
||||||
_isPinching = false;
|
_isPinching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,105 +104,146 @@ function _pdfTouchEnd(e) {
|
||||||
const ce = $('reader-content');
|
const ce = $('reader-content');
|
||||||
if (ce) ce.classList.remove('pinch-active');
|
if (ce) ce.classList.remove('pinch-active');
|
||||||
const vp = document.getElementById('pdf-viewport');
|
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));
|
const snapped = Math.max(50, Math.min(200, Math.round(liveZoom / 10) * 10));
|
||||||
applyPdfZoom(snapped);
|
applyPdfZoom(snapped);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!readerSettings.pdfPaginated) return;
|
if (!readerSettings.pdfPaginated || !e.changedTouches.length) return;
|
||||||
const delta = e.changedTouches[0].clientX - _touchStartX;
|
const dx = e.changedTouches[0].clientX - _touchStartX;
|
||||||
if (delta > 50) pdfGoToPage(pdfCurrentPage - 1);
|
const dy = e.changedTouches[0].clientY - _touchStartY;
|
||||||
else if (delta < -50) pdfGoToPage(pdfCurrentPage + 1);
|
// 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) {
|
function pdfGoToPage(n) {
|
||||||
if (!currentPdfDoc) return;
|
if (!currentPdfDoc) return;
|
||||||
n = Math.max(1, Math.min(pdfTotalPages, n));
|
n = Math.max(1, Math.min(pdfTotalPages, n | 0));
|
||||||
if (n === pdfCurrentPage) return;
|
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
|
if (!readerSettings.pdfPaginated) {
|
||||||
const oldWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`);
|
const target = contentEl.querySelector(`#pdf-page-${n}`);
|
||||||
if (oldWrapper) oldWrapper.style.display = 'none';
|
if (target) {
|
||||||
|
if (typeof _suppressScrollJumpDetect === 'function') _suppressScrollJumpDetect();
|
||||||
pdfCurrentPage = n;
|
contentEl.scrollTop += target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top - 8;
|
||||||
const newWrapper = contentEl.querySelector(`#pdf-page-${pdfCurrentPage}`);
|
}
|
||||||
if (newWrapper) newWrapper.style.display = '';
|
_pdfSetCurrentPage(n);
|
||||||
|
return;
|
||||||
pdfSmartZoomPage(pdfCurrentPage);
|
}
|
||||||
|
if (n === pdfCurrentPage && _pdfView && _pdfView.shown === n) return;
|
||||||
const progressInput = $('reader-progress-input');
|
_pdfShowPage(n);
|
||||||
if (progressInput) progressInput.value = pdfCurrentPage;
|
_pdfTurned();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pdfSmartZoomPage(pageNum) {
|
// A turn is reading: the place is saved soon (not only by the 10 s timer), and e-readers
|
||||||
if (!currentPdfDoc) return;
|
// get their refresh (native-eink.js listens).
|
||||||
const contentEl = $('reader-content');
|
function _pdfTurned() {
|
||||||
if (!contentEl) return;
|
if (typeof _noteReaderInteraction === 'function') _noteReaderInteraction();
|
||||||
const wrapper = contentEl.querySelector(`#pdf-page-${pageNum}`);
|
clearTimeout(_pdfTurnSaveTimer);
|
||||||
if (!wrapper) return;
|
_pdfTurnSaveTimer = setTimeout(() => { if (currentPdfDoc) saveReaderProgress(); }, 1500);
|
||||||
const canvas = wrapper.querySelector('canvas');
|
try { window.dispatchEvent(new CustomEvent('diora-pdf-turn', {detail: {page: pdfCurrentPage}})); } catch (e) {}
|
||||||
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;
|
/** Show page [n] alone, cropped; draw the next one ahead and let those far away go. */
|
||||||
const containerH = contentEl.clientHeight;
|
async function _pdfShowPage(n) {
|
||||||
const contentW = bbox.x2 - bbox.x1;
|
const v = _pdfView;
|
||||||
const contentH = bbox.y2 - bbox.y1;
|
if (!v || !v.entries.length) return;
|
||||||
const pad = 12;
|
n = Math.max(1, Math.min(v.entries.length, n));
|
||||||
const scale = Math.min(
|
_pdfSetCurrentPage(n);
|
||||||
(containerW - pad * 2) / contentW,
|
v.shown = n;
|
||||||
(containerH - pad * 2) / contentH
|
const token = ++v.showToken;
|
||||||
) * (readerSettings.pdfZoom / 100);
|
v.entries.forEach(e => { e.wrapper.style.display = e.num === n ? '' : 'none'; });
|
||||||
|
try {
|
||||||
// Re-render canvas at new scale if significantly different
|
await _pdfFitPage(v, v.entries[n - 1]);
|
||||||
const currentScale = canvas.width / naturalVp.width;
|
if (token !== v.showToken || v.stopped) return;
|
||||||
if (Math.abs(scale - currentScale) / currentScale > 0.05) {
|
if (n < v.entries.length) await _pdfFitPage(v, v.entries[n], true);
|
||||||
const vp = page.getViewport({scale});
|
if (token !== v.showToken || v.stopped) return;
|
||||||
canvas.width = vp.width;
|
if (n > 1 && !v.entries[n - 2].drawn) await _pdfFitPage(v, v.entries[n - 2], true);
|
||||||
canvas.height = vp.height;
|
} catch (err) {
|
||||||
await page.render({canvasContext: canvas.getContext('2d'), viewport: vp}).promise;
|
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); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position canvas to center the text bounding box
|
/**
|
||||||
const renderedScale = canvas.width / naturalVp.width;
|
* Page [e] sized to the screen by its text (its ink, for scans): drawn when the size differs,
|
||||||
const offsetX = -renderedScale * (bbox.x1 - pad) + (containerW - renderedScale * contentW - pad * 2) / 2;
|
* then placed so the text is centred. [ahead]: for the next turn, not shown yet.
|
||||||
// PDF y-axis is bottom-up; canvas is top-down
|
*/
|
||||||
const offsetY = -renderedScale * (pageH - bbox.y2 - pad) + (containerH - renderedScale * contentH - pad * 2) / 2;
|
async function _pdfFitPage(v, e, ahead) {
|
||||||
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
const contentEl = v.contentEl;
|
||||||
wrapper.style.overflow = 'hidden';
|
const page = await v.pdf.getPage(e.num);
|
||||||
wrapper.style.width = containerW + 'px';
|
if (v.stopped) return;
|
||||||
wrapper.style.height = containerH + 'px';
|
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 = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _computePdfTextBox(page, pageW, pageH) {
|
/**
|
||||||
|
* 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
|
// Tier 1: text-based
|
||||||
try {
|
try {
|
||||||
const tc = await page.getTextContent();
|
const tc = await page.getTextContent();
|
||||||
if (tc.items && tc.items.length) {
|
if (tc.items && tc.items.length) {
|
||||||
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
||||||
for (const item of tc.items) {
|
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 tx = item.transform[4], ty = item.transform[5];
|
||||||
const iw = item.width || 0, ih = item.height || 0;
|
const iw = item.width || 0, ih = item.height || Math.abs(item.transform[3]) || 0;
|
||||||
if (tx < x1) x1 = tx;
|
for (const [px, py] of [[tx, ty], [tx + iw, ty + ih]]) {
|
||||||
if (ty < y1) y1 = ty;
|
const [vx, vy] = natural.convertToViewportPoint(px, py);
|
||||||
if (tx + iw > x2) x2 = tx + iw;
|
if (vx < x1) x1 = vx;
|
||||||
if (ty + ih > y2) y2 = ty + ih;
|
if (vx > x2) x2 = vx;
|
||||||
|
if (vy < y1) y1 = vy;
|
||||||
|
if (vy > y2) y2 = vy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const area = (x2 - x1) * (y2 - y1);
|
const area = (x2 - x1) * (y2 - y1);
|
||||||
if (isFinite(x1) && area > pageW * pageH * 0.25) {
|
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) {}
|
} catch (e) {}
|
||||||
|
|
@ -179,11 +253,12 @@ async function _computePdfTextBox(page, pageW, pageH) {
|
||||||
const lowScale = 0.3;
|
const lowScale = 0.3;
|
||||||
const vp = page.getViewport({scale: lowScale});
|
const vp = page.getViewport({scale: lowScale});
|
||||||
const offCanvas = document.createElement('canvas');
|
const offCanvas = document.createElement('canvas');
|
||||||
offCanvas.width = vp.width;
|
offCanvas.width = Math.ceil(vp.width);
|
||||||
offCanvas.height = vp.height;
|
offCanvas.height = Math.ceil(vp.height);
|
||||||
const ctx = offCanvas.getContext('2d');
|
const ctx = offCanvas.getContext('2d');
|
||||||
await page.render({canvasContext: ctx, viewport: vp}).promise;
|
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;
|
let rMin = height, rMax = 0, cMin = width, cMax = 0;
|
||||||
for (let r = 0; r < height; r++) {
|
for (let r = 0; r < height; r++) {
|
||||||
|
|
@ -198,12 +273,7 @@ async function _computePdfTextBox(page, pageW, pageH) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rMin < rMax && cMin < cMax) {
|
if (rMin < rMax && cMin < cMax) {
|
||||||
return {
|
return {x1: cMin / lowScale, y1: rMin / lowScale, x2: cMax / lowScale, y2: rMax / lowScale};
|
||||||
x1: cMin / lowScale,
|
|
||||||
y1: (height - rMax) / lowScale,
|
|
||||||
x2: cMax / lowScale,
|
|
||||||
y2: (height - rMin) / lowScale,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,15 +143,7 @@ function jumpToBookmark(id) {
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl) return;
|
if (!contentEl) return;
|
||||||
if (bm.anchor.startsWith('pdf-page-')) {
|
if (bm.anchor.startsWith('pdf-page-')) {
|
||||||
if (readerSettings.pdfPaginated) {
|
|
||||||
pdfGoToPage(parseInt(bm.anchor.replace('pdf-page-', ''), 10) || 1);
|
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'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const target = Array.from(contentEl.querySelectorAll('[data-epub-src]'))
|
const target = Array.from(contentEl.querySelectorAll('[data-epub-src]'))
|
||||||
.find(el => el.getAttribute('data-epub-src') === bm.anchor);
|
.find(el => el.getAttribute('data-epub-src') === bm.anchor);
|
||||||
|
|
|
||||||
|
|
@ -122,16 +122,20 @@
|
||||||
|
|
||||||
async function pdfCover(bytes) {
|
async function pdfCover(bytes) {
|
||||||
if (typeof pdfjsLib === 'undefined') return null;
|
if (typeof pdfjsLib === 'undefined') return null;
|
||||||
const pdf = await pdfjsLib.getDocument({data: new Uint8Array(bytes)}).promise;
|
const pdf = await (typeof _pdfLoad === 'function' ? _pdfLoad(new Uint8Array(bytes))
|
||||||
|
: pdfjsLib.getDocument({data: new Uint8Array(bytes)})).promise;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
try {
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
const v = page.getViewport({scale: 1});
|
const v = page.getViewport({scale: 1});
|
||||||
const scale = Math.min(MAX_W / v.width, MAX_H / v.height);
|
const scale = Math.min(MAX_W / v.width, MAX_H / v.height);
|
||||||
const viewport = page.getViewport({scale});
|
const viewport = page.getViewport({scale});
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = Math.round(viewport.width);
|
canvas.width = Math.round(viewport.width);
|
||||||
canvas.height = Math.round(viewport.height);
|
canvas.height = Math.round(viewport.height);
|
||||||
await page.render({canvasContext: canvas.getContext('2d'), viewport}).promise;
|
await page.render({canvasContext: canvas.getContext('2d'), viewport}).promise;
|
||||||
pdf.destroy();
|
} finally {
|
||||||
|
try { pdf.destroy(); } catch (e) {}
|
||||||
|
}
|
||||||
return new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', QUALITY));
|
return new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', QUALITY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue