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