comparison mupdf-source/platform/wasm/examples/simple-viewer/index.html @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
comparison
equal deleted inserted replaced
1:1d09e1dec1d9 2:b50eed0cc0ef
1 <!DOCTYPE html>
2
3 <!--
4 Copyright (C) 2022, 2024, 2025 Artifex Software, Inc.
5
6 This file is part of MuPDF.
7
8 MuPDF is free software: you can redistribute it and/or modify it under the
9 terms of the GNU Affero General Public License as published by the Free
10 Software Foundation, either version 3 of the License, or (at your option)
11 any later version.
12
13 MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16 details.
17
18 You should have received a copy of the GNU Affero General Public License
19 along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
20
21 Alternative licensing terms are available from the licensor.
22 For commercial licensing, see <https://www.artifex.com/> or contact
23 Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
24 CA 94129, USA, for further information.
25 -->
26
27 <title>MuPDF.js</title>
28
29 <meta charset="utf-8">
30 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
31
32 <link rel="shortcut icon" href="favicon.svg">
33
34 <style>
35 * {
36 box-sizing: border-box;
37 }
38
39 /* APPEARANCE */
40
41 html {
42 font-family: sans-serif;
43 font-size: 18px;
44 background-color: gray;
45 }
46
47 header {
48 border-bottom: 1px solid black;
49 background-color: gainsboro;
50 }
51
52 footer {
53 border-top: 1px solid black;
54 background-color: gainsboro;
55 }
56
57 aside {
58 background-color: white;
59 border-right: 1px solid black;
60 }
61
62 #message {
63 text-align: center;
64 font-size: 24pt;
65 font-weight: bold;
66 color: silver;
67 }
68
69 details[open] > summary {
70 background-color: #0004;
71 }
72
73 menu {
74 min-width: 140px;
75 border: 1px solid black;
76 background-color: white;
77 color: black;
78 }
79
80 menu li:hover {
81 background-color: black;
82 color: white;
83 }
84
85 /* LAYOUT */
86
87 html {
88 margin: 0;
89 padding: 0;
90 width: 100%;
91 height: 100%;
92 }
93
94 body {
95 margin: 0;
96 padding: 0;
97 display: grid;
98 grid-template-columns: auto minmax(0, 1fr);
99 grid-template-rows: auto minmax(0, 1fr) auto;
100 width: 100%;
101 height: 100%;
102 overflow: clip;
103 }
104
105 header {
106 position: relative;
107 user-select: none;
108 grid-column: 1/3;
109 grid-row: 1;
110 display: flex;
111 flex-wrap: wrap;
112 }
113
114 footer {
115 grid-column: 1/3;
116 grid-row: 3;
117 display: flex;
118 padding: 8px;
119 gap: 8px;
120 }
121
122 aside {
123 grid-column: 1;
124 grid-row: 2;
125 overflow-y: auto;
126 width: 250px;
127 }
128
129 main {
130 grid-column: 2;
131 grid-row: 2;
132 overflow: scroll;
133 }
134
135 summary {
136 padding: 4px 8px;
137 cursor: pointer;
138 list-style: none;
139 }
140
141 /* workaround for bug in Safari details appearance */
142 summary::-webkit-details-marker {
143 display: none;
144 }
145
146 menu {
147 position: absolute;
148 overflow-y: auto;
149 margin: 0;
150 padding: 0;
151 list-style: none;
152 z-index: 500;
153 }
154
155 menu li {
156 padding: 4px 8px;
157 cursor: pointer;
158 }
159
160 /* OUTLINE */
161
162 #outline {
163 font-size: 12px;
164 }
165
166 #outline ul {
167 margin: 0;
168 padding-left: 20px;
169 }
170
171 #outline a {
172 color: black;
173 text-decoration: none;
174 }
175
176 #outline a:hover {
177 color: blue;
178 text-decoration: underline;
179 }
180
181 /* PAGES */
182
183 #pages {
184 margin: 0 auto;
185 }
186
187 div.page {
188 position: relative;
189 background-color: white;
190 margin: 16px auto;
191 box-shadow: 0px 2px 8px #0004;
192 }
193
194 div.page * {
195 position: absolute;
196 }
197
198 div.page canvas {
199 user-select: none;
200 }
201
202 svg.text {
203 width: 100%;
204 height: 100%;
205 }
206
207 svg.text text {
208 white-space: pre;
209 line-height: 1;
210 fill: transparent;
211 }
212
213 svg.text ::selection {
214 background: hsla(220, 100%, 50%, 0.2);
215 color: transparent;
216 }
217
218 div.link a:hover {
219 border: 1px dotted blue;
220 }
221
222 #pages.do-content-select div.link {
223 pointer-events: none;
224 }
225
226 div.search > div {
227 pointer-events: none;
228 border: 1px solid hotpink;
229 background-color: lightpink;
230 mix-blend-mode: multiply;
231 }
232 </style>
233
234 <body>
235
236 <header id="menubar-panel">
237 <details>
238 <summary>File</summary>
239 <menu>
240 <li onclick="document.getElementById('open-file-input').click()">Open File...
241 </menu>
242 </details>
243 <details>
244 <summary>Edit</summary>
245 <menu>
246 <li onclick="show_search_panel()">Search...
247 </menu>
248 </details>
249 <details>
250 <summary>View</summary>
251 <menu>
252 <li onclick="toggle_fullscreen()">Fullscreen
253 <li onclick="toggle_outline_panel()">Outline
254 <li onclick="zoom_to(48)">50%
255 <li onclick="zoom_to(72)">75% (72 dpi)
256 <li onclick="zoom_to(96)">100% (96 dpi)
257 <li onclick="zoom_to(120)">125%
258 <li onclick="zoom_to(144)">150%
259 <li onclick="zoom_to(192)">200%
260 </menu>
261 </details>
262 </header>
263
264 <aside id="outline-panel" style="display:none">
265 <ul id="outline">
266 <!-- outline inserted here -->
267 </ul>
268 </aside>
269
270 <main id="page-panel">
271 <div id="message">
272 Loading MuPDF.js...
273 </div>
274 <div id="pages">
275 <!-- pages inserted here -->
276 </div>
277 </main>
278
279 <footer id="search-panel" style="display:none">
280 <input
281 id="search-input"
282 type="search"
283 size="40"
284 placeholder="Search..."
285 >
286 <button id="search-prev" onclick="run_search(-1, 1)">&#x3C;</button>
287 <button id="search-next" onclick="run_search(1, 1)">&#x3E;</button>
288 <div id="search-status" style="flex-grow:1"></div>
289 <button onclick="hide_search_panel()">X</button>
290 </footer>
291
292 <!-- hidden input for file dialog -->
293 <input
294 style="display: none"
295 id="open-file-input"
296 type="file"
297 accept=".pdf,application/pdf"
298 onchange="open_document_from_file(event.target.files[0])"
299 >
300
301 </body>
302
303 <script>
304 "use strict"
305
306 // FAST SORTED ARRAY FUNCTIONS
307
308 function array_remove(array, index) {
309 let n = array.length
310 for (let i = index + 1; i < n; ++i)
311 array[i - 1] = array[i]
312 array.length = n - 1
313 }
314
315 function array_insert(array, index, item) {
316 for (let i = array.length; i > index; --i)
317 array[i] = array[i - 1]
318 array[index] = item
319 }
320
321 function set_has(set, item) {
322 let a = 0
323 let b = set.length - 1
324 while (a <= b) {
325 let m = (a + b) >> 1
326 let x = set[m]
327 if (item < x)
328 b = m - 1
329 else if (item > x)
330 a = m + 1
331 else
332 return true
333 }
334 return false
335 }
336
337 function set_add(set, item) {
338 let a = 0
339 let b = set.length - 1
340 while (a <= b) {
341 let m = (a + b) >> 1
342 let x = set[m]
343 if (item < x)
344 b = m - 1
345 else if (item > x)
346 a = m + 1
347 else
348 return
349 }
350 array_insert(set, a, item)
351 }
352
353 function set_delete(set, item) {
354 let a = 0
355 let b = set.length - 1
356 while (a <= b) {
357 let m = (a + b) >> 1
358 let x = set[m]
359 if (item < x)
360 b = m - 1
361 else if (item > x)
362 a = m + 1
363 else {
364 array_remove(set, m)
365 return
366 }
367 }
368 }
369
370 // LOADING AND ERROR MESSAGES
371
372 function show_message(msg) {
373 document.getElementById("message").textContent = msg
374 }
375
376 function clear_message() {
377 document.getElementById("message").textContent = ""
378 }
379
380 // MENU BAR
381
382 function close_all_menus(self) {
383 for (let node of document.querySelectorAll("header > details"))
384 if (node !== self)
385 node.removeAttribute("open")
386 }
387
388 /* close menu if opening another */
389 for (let node of document.querySelectorAll("header > details")) {
390 node.addEventListener("click", function () {
391 close_all_menus(node)
392 })
393 }
394
395 /* close menu after selecting something */
396 for (let node of document.querySelectorAll("header > details > menu")) {
397 node.addEventListener("click", function () {
398 close_all_menus(null)
399 })
400 }
401
402 /* click anywhere outside the menu to close it */
403 window.addEventListener("mousedown", function (evt) {
404 let e = evt.target
405 while (e) {
406 if (e.tagName === "DETAILS")
407 return
408 e = e.parentElement
409 }
410 close_all_menus(null)
411 })
412
413 /* close menus if window loses focus */
414 window.addEventListener("blur", function () {
415 close_all_menus(null)
416 })
417
418 // BACKGROUND WORKER
419
420 const worker = new Worker("worker.js", { type: "module" })
421
422 worker._promise_id = 1
423 worker._promise_map = new Map()
424
425 worker.wrap = function (name) {
426 return function (...args) {
427 return new Promise(function (resolve, reject) {
428 let id = worker._promise_id++
429 worker._promise_map.set(id, { resolve, reject })
430 if (args[0] instanceof ArrayBuffer)
431 worker.postMessage([ name, id, args ], [ args[0] ])
432 else
433 worker.postMessage([ name, id, args ])
434 })
435 }
436 }
437
438 worker.onmessage = function (event) {
439 let [ type, id, result ] = event.data
440 let error
441
442 switch (type) {
443 case "INIT":
444 for (let method of result)
445 worker[method] = worker.wrap(method)
446 main()
447 break
448
449 case "RESULT":
450 worker._promise_map.get(id).resolve(result)
451 worker._promise_map.delete(id)
452 break
453
454 case "ERROR":
455 error = new Error(result.message)
456 error.name = result.name
457 error.stack = result.stack
458 worker._promise_map.get(id).reject(error)
459 worker._promise_map.delete(id)
460 break
461
462 default:
463 error = new Error(`Invalid message: ${type}`)
464 worker._promise_map.get(id).reject(error)
465 break
466 }
467 }
468
469 // PAGE VIEW
470
471 class PageView {
472 constructor(doc, pageNumber, defaultSize, zoom) {
473 this.doc = doc
474 this.pageNumber = pageNumber // 0-based
475 this.size = defaultSize
476
477 this.loadPromise = false
478 this.drawPromise = false
479
480 this.rootNode = document.createElement("div")
481 this.rootNode.id = "page" + (pageNumber + 1)
482 this.rootNode.className = "page"
483 this.rootNode.page = this
484
485 this.canvasNode = document.createElement("canvas")
486 this.canvasCtx = this.canvasNode.getContext("2d")
487 this.rootNode.appendChild(this.canvasNode)
488
489 this.textData = null
490 this.textNode = document.createElementNS("http://www.w3.org/2000/svg", "svg")
491 this.textNode.classList.add("text")
492 this.rootNode.appendChild(this.textNode)
493
494 this.linkData = null
495 this.linkNode = document.createElement("div")
496 this.linkNode.className = "link"
497 this.rootNode.appendChild(this.linkNode)
498
499 this.needle = null
500 this.loadNeedle = null
501 this.showNeedle = null
502
503 this.searchData = null
504 this.searchNode = document.createElement("div")
505 this.searchNode.className = "search"
506 this.rootNode.appendChild(this.searchNode)
507
508 this.zoom = zoom
509 this._updateSize()
510 }
511
512 // Update page element size for current zoom level.
513 _updateSize() {
514 // We Math.ceil to match the behavior of fz_irect_from_rect that is used by the worker.
515 this.rootNode.style.width = Math.ceil((this.size.width * this.zoom) / 72) + "px"
516 this.rootNode.style.height = Math.ceil((this.size.height * this.zoom) / 72) + "px"
517 this.canvasNode.style.width = Math.ceil((this.size.width * this.zoom) / 72) + "px"
518 this.canvasNode.style.height = Math.ceil((this.size.height * this.zoom) / 72) + "px"
519 }
520
521 setZoom(zoom) {
522 if (this.zoom !== zoom) {
523 this.zoom = zoom
524 this._updateSize()
525 }
526 }
527
528 setSearch(needle) {
529 if (this.needle !== needle)
530 this.needle = needle
531 }
532
533 async _load() {
534 console.log("LOADING", this.pageNumber)
535
536 this.size = await worker.getPageSize(this.doc, this.pageNumber)
537 this.textData = await worker.getPageText(this.doc, this.pageNumber)
538 this.linkData = await worker.getPageLinks(this.doc, this.pageNumber)
539
540 this._updateSize()
541 }
542
543 async _loadSearch() {
544 if (this.loadNeedle !== this.needle) {
545 this.loadNeedle = this.needle
546 if (!this.needle)
547 this.searchData = null
548 else
549 this.searchData = await worker.search(this.doc, this.pageNumber, this.needle)
550 }
551 }
552
553 async _show() {
554 if (!this.loadPromise)
555 this.loadPromise = this._load()
556 await this.loadPromise
557
558 // Render image if zoom factor has changed!
559 if (this.canvasNode.zoom !== this.zoom)
560 this._render()
561
562 // (Re-)create HTML nodes if zoom factor has changed
563 if (this.textNode.zoom !== this.zoom)
564 this._showText()
565
566 // (Re-)create HTML nodes if zoom factor has changed
567 if (this.linkNode.zoom !== this.zoom)
568 this._showLinks()
569
570 // Reload search hits if the needle has changed.
571 // TODO: race condition with multiple queued searches
572 if (this.loadNeedle !== this.needle)
573 await this._loadSearch()
574
575 // (Re-)create HTML nodes if search changed or zoom factor changed
576 if (this.showNeedle !== this.needle || this.searchNode.zoom !== this.zoom)
577 this._showSearch()
578 }
579
580 async _render() {
581 // Remember zoom value when we start rendering.
582 let zoom = this.zoom
583
584 // If the current image node was rendered with the same arguments we skip the render.
585 if (this.canvasNode.zoom === this.zoom)
586 return
587
588 if (this.drawPromise) {
589 // If a render is ongoing, don't queue a new render immediately!
590 // When the on-going render finishes, we check the page zoom value.
591 // If it is stale, we immediately queue a new render.
592 console.log("BUSY DRAWING", this.pageNumber)
593 return
594 }
595
596 console.log("DRAWING", this.pageNumber, zoom)
597
598 this.canvasNode.zoom = this.zoom
599
600 this.drawPromise = worker.drawPageAsPixmap(this.doc, this.pageNumber, zoom * devicePixelRatio)
601
602 let imageData = await this.drawPromise
603 if (imageData == null)
604 return
605
606 this.drawPromise = null
607
608 if (this.zoom === zoom) {
609 // Render is still valid. Use it!
610 console.log("FRESH IMAGE", this.pageNumber)
611 this.canvasNode.width = imageData.width
612 this.canvasNode.height = imageData.height
613 this.canvasCtx.putImageData(imageData, 0, 0)
614 } else {
615 // Uh-oh. This render is already stale. Try again!
616 console.log("STALE IMAGE", this.pageNumber)
617 if (set_has(page_visible, this.pageNumber))
618 this._render()
619 }
620 }
621
622 _showText() {
623 let frag = document.createDocumentFragment()
624 let scale = this.zoom / 72
625
626 for (let block of this.textData.blocks) {
627 if (block.type === "text") {
628 for (let line of block.lines) {
629 let text = document.createElementNS("http://www.w3.org/2000/svg", "text")
630 text.setAttribute("x", line.bbox.x * scale + "px")
631 text.setAttribute("y", line.y * scale + "px")
632 text.style.fontSize = line.font.size * scale + "px"
633 text.style.fontFamily = line.font.family
634 text.style.fontWeight = line.font.weight
635 text.style.fontStyle = line.font.style
636 text.setAttribute("textLength", line.bbox.w * scale + "px")
637 text.setAttribute("lengthAdjust", "spacingAndGlyphs")
638 text.textContent = line.text
639 frag.appendChild(text)
640 }
641 }
642 }
643
644 this.textNode.zoom = this.zoom
645 this.textNode.replaceChildren(frag)
646 }
647
648 _showLinks() {
649 this.linkNode.zoom = this.zoom
650 this.linkNode.replaceChildren()
651
652 let scale = this.zoom / 72
653 for (let link of this.linkData) {
654 let a = document.createElement("a")
655 a.href = link.href
656 a.style.left = link.x * scale + "px"
657 a.style.top = link.y * scale + "px"
658 a.style.width = link.w * scale + "px"
659 a.style.height = link.h * scale + "px"
660 this.linkNode.appendChild(a)
661 }
662 }
663
664 _showSearch() {
665 this.showNeedle = this.needle
666 this.searchNode.zoom = this.zoom
667 this.searchNode.replaceChildren()
668
669 if (this.searchData) {
670 let scale = this.zoom / 72
671 for (let bbox of this.searchData) {
672 let div = document.createElement("div")
673 div.style.left = bbox.x * scale + "px"
674 div.style.top = bbox.y * scale + "px"
675 div.style.width = bbox.w * scale + "px"
676 div.style.height = bbox.h * scale + "px"
677 this.searchNode.appendChild(div)
678 }
679 }
680 }
681 }
682
683 // DOCUMENT VIEW
684
685 var current_doc = 0
686 var current_zoom = 96
687
688 var page_list = null // all pages in document
689
690 // Track page visibility as the user scrolls through the document.
691 // When a page comes near the viewport, we add it to the list of
692 // "visible" pages and queue up rendering it.
693 var page_visible = []
694 var page_observer = new IntersectionObserver(
695 function (entries) {
696 for (let entry of entries) {
697 let page = entry.target.page
698 if (entry.isIntersecting)
699 set_add(page_visible, page.pageNumber)
700 else
701 set_delete(page_visible, page.pageNumber)
702 }
703 queue_update_view()
704 },
705 {
706 // This means we have 3 viewports of vertical "head start" where
707 // the page is rendered before it becomes visible.
708 root: document.getElementById("page-panel"),
709 rootMargin: "25% 0px 300% 0px",
710 }
711 )
712
713
714 // Timer that waits until things settle before kicking off rendering.
715 var update_view_timer = 0
716 function queue_update_view() {
717 if (update_view_timer)
718 clearTimeout(update_view_timer)
719 update_view_timer = setTimeout(update_view, 50)
720 }
721
722 function update_view() {
723 if (update_view_timer)
724 clearTimeout(update_view_timer)
725 update_view_timer = 0
726
727 for (let i of page_visible)
728 page_list[i]._show()
729 }
730
731 function find_visible_page() {
732 let panel = document.getElementById("page-panel").getBoundingClientRect()
733 let panel_mid = (panel.top + panel.bottom) / 2
734 for (let p of page_visible) {
735 let rect = page_list[p].rootNode.getBoundingClientRect()
736 if (rect.top <= panel_mid && rect.bottom >= panel_mid)
737 return p
738 }
739 return page_visible[0]
740 }
741
742 function zoom_in() {
743 zoom_to(Math.min(current_zoom + 12, 384))
744 }
745
746 function zoom_out() {
747 zoom_to(Math.max(current_zoom - 12, 48))
748 }
749
750 function zoom_to(new_zoom) {
751 if (current_zoom === new_zoom)
752 return
753 current_zoom = new_zoom
754
755 // TODO: keep page coord at center of cursor in place when zooming
756
757 let p = find_visible_page()
758
759 for (let page of page_list)
760 page.setZoom(current_zoom)
761
762 page_list[p].rootNode.scrollIntoView()
763
764 queue_update_view()
765 }
766
767 // KEY BINDINGS & MOUSE WHEEL ZOOM
768
769 window.addEventListener("wheel",
770 function (event) {
771 // Intercept Ctl+MOUSEWHEEL that change browser zoom.
772 // Our page rendering requires a 1-to-1 pixel scale.
773 if (event.ctrlKey || event.metaKey) {
774 if (event.deltaY < 0)
775 zoom_in()
776 else if (event.deltaY > 0)
777 zoom_out()
778 event.preventDefault()
779 }
780 },
781 { passive: false }
782 )
783
784 window.addEventListener("keydown", function (event) {
785 // Intercept and override some keyboard shortcuts.
786 // We must override the Ctl-PLUS and Ctl-MINUS shortcuts that change browser zoom.
787 // Our page rendering requires a 1-to-1 pixel scale.
788 if (event.ctrlKey || event.metaKey) {
789 switch (event.keyCode) {
790 // '=' / '+' on various keyboards
791 case 61:
792 case 107:
793 case 187:
794 case 171:
795 zoom_in()
796 event.preventDefault()
797 break
798 // '-'
799 case 173:
800 case 109:
801 case 189:
802 zoom_out()
803 event.preventDefault()
804 break
805 // '0'
806 case 48:
807 case 96:
808 zoom_to(100)
809 break
810 // 'A'
811 case 65:
812 // Ctrl-A full selection
813 document.getSelection().selectAllChildren(document.getElementById("pages"))
814 event.preventDefault()
815 break
816 // 'F'
817 case 70:
818 show_search_panel()
819 event.preventDefault()
820 break
821
822 // 'G'
823 case 71:
824 show_search_panel()
825 run_search(event.shiftKey ? -1 : 1, 1)
826 event.preventDefault()
827 break
828 }
829 }
830
831 if (event.key === "Escape") {
832 hide_search_panel()
833 }
834 })
835
836 function toggle_fullscreen() {
837 // Safari on iPhone doesn't support Fullscreen
838 if (typeof document.documentElement.requestFullscreen !== "function")
839 return
840 if (document.fullscreenElement)
841 document.exitFullscreen()
842 else
843 document.documentElement.requestFullscreen()
844 }
845
846 // Mark TEXT-SELECTION State
847 function remove_selection_state(e) {
848 document.getElementById("pages").classList.remove("do-content-select")
849 document.removeEventListener("mouseup", remove_selection_state)
850 }
851
852 document.addEventListener("selectstart", function (event) {
853 document.getElementById("pages").classList.add("do-content-select")
854 document.addEventListener("mouseup", remove_selection_state)
855 })
856
857 // SEARCH
858
859 let search_panel = document.getElementById("search-panel")
860 let search_status = document.getElementById("search-status")
861 let search_input = document.getElementById("search-input")
862
863 var current_search_needle = ""
864 var last_search_page = -1
865
866 search_input.onchange = function (event) {
867 last_search_page = -1
868 }
869
870 search_input.onkeyup = function (event) {
871 if (event.key === 'Enter') {
872 if (event.shiftKey)
873 document.getElementById("search-prev").click()
874 else
875 document.getElementById("search-next").click()
876 }
877 }
878
879 function show_search_panel() {
880 if (!page_list)
881 return
882 search_panel.style.display = ""
883 search_input.focus()
884 search_input.select()
885 }
886
887 function hide_search_panel() {
888 search_panel.style.display = "none"
889 search_input.value = ""
890 set_search_needle("")
891 }
892
893 function set_search_needle(needle) {
894 search_status.textContent = ""
895 current_search_needle = needle
896
897 if (!page_list)
898 return
899
900 for (let page of page_list)
901 page.setSearch(current_search_needle)
902
903 queue_update_view()
904 }
905
906 async function run_search(direction, step) {
907 // start search from visible page
908 set_search_needle(search_input.value)
909
910 let page = 0;
911 if (last_search_page === -1)
912 page = find_visible_page()
913 else {
914 page = last_search_page
915 if (step)
916 page += direction
917 }
918
919 while (page >= 0 && page < page_list.length) {
920 // We run the check once per loop iteration,
921 // in case the search was cancel during the 'await' below.
922 if (current_search_needle === "") {
923 search_status.textContent = ""
924 return
925 }
926
927 search_status.textContent = `Searching page ${page + 1}.`
928
929 if (page_list[page].loadNeedle !== page_list[page].needle)
930 await page_list[page]._loadSearch()
931
932 const hits = page_list[page].searchData
933 if (hits && hits.length > 0) {
934 page_list[page].rootNode.scrollIntoView()
935 last_search_page = page
936 const word = hits.length === 1 ? "hit" : "hits"
937 search_status.textContent = `${hits.length} ${word} on page ${page + 1}.`
938 return
939 }
940
941 page += direction
942 }
943
944 search_status.textContent = "No more search hits."
945 }
946
947 // OUTLINE
948
949 function build_outline(parent, outline) {
950 for (let item of outline) {
951 let node = document.createElement("li")
952 let a = document.createElement("a")
953 a.href = "#page" + (item.page + 1)
954 a.textContent = item.title
955 node.appendChild(a)
956 if (item.down) {
957 let down = document.createElement("ul")
958 build_outline(down, item.down)
959 node.appendChild(down)
960 }
961 parent.appendChild(node)
962 }
963 }
964
965 function toggle_outline_panel() {
966 if (document.getElementById("outline-panel").style.display === "none")
967 show_outline_panel()
968 else
969 hide_outline_panel()
970 }
971
972 function show_outline_panel() {
973 if (!page_list)
974 return
975 document.getElementById("outline-panel").style.display = "block"
976 }
977
978 function hide_outline_panel() {
979 document.getElementById("outline-panel").style.display = "none"
980 }
981
982 // DOCUMENT LOADING
983
984 function close_document() {
985 clear_message()
986 hide_outline_panel()
987 hide_search_panel()
988
989 if (current_doc) {
990 worker.closeDocument(current_doc)
991 current_doc = 0
992 document.getElementById("outline").replaceChildren()
993 document.getElementById("pages").replaceChildren()
994 for (let page of page_list)
995 page_observer.unobserve(page.rootNode)
996 page_visible.length = 0
997 }
998
999 page_list = null
1000 }
1001
1002 async function init_document(title) {
1003 document.title = await worker.documentTitle(current_doc) || title
1004
1005 var page_count = await worker.countPages(current_doc)
1006
1007 // Use second page as default page size (the cover page is often differently sized)
1008 var page_size = await worker.getPageSize(current_doc, page_count > 1 ? 1 : 0)
1009
1010 page_list = []
1011 for (let i = 0; i < page_count; ++i)
1012 page_list[i] = new PageView(current_doc, i, page_size, current_zoom)
1013
1014 for (let page of page_list) {
1015 document.getElementById("pages").appendChild(page.rootNode)
1016 page_observer.observe(page.rootNode)
1017 }
1018
1019 var outline = await worker.documentOutline(current_doc)
1020 if (outline) {
1021 build_outline(document.getElementById("outline"), outline)
1022 show_outline_panel()
1023 } else {
1024 hide_outline_panel()
1025 }
1026
1027 clear_message()
1028
1029 current_search_needle = ""
1030 last_search_page = -1
1031 }
1032
1033 async function open_document_from_buffer(buffer, magic, title) {
1034 current_doc = await worker.openDocumentFromBuffer(buffer, magic)
1035 await init_document(title)
1036 }
1037
1038 async function open_document_from_blob(blob, magic, title) {
1039 current_doc = await worker.openDocumentFromBlob(blob, magic)
1040 await init_document(title)
1041 }
1042
1043 async function open_document_from_file(file) {
1044 close_document()
1045 try {
1046 show_message("Opening " + file.name)
1047 history.replaceState(null, null, window.location.pathname)
1048 await open_document_from_blob(file, file.name, file.name)
1049 } catch (error) {
1050 show_message(error.name + ": " + error.message)
1051 console.error(error)
1052 }
1053 }
1054
1055 async function open_document_from_url(path) {
1056 close_document()
1057 try {
1058 show_message("Loading " + path)
1059 let response = await fetch(path)
1060 if (!response.ok)
1061 throw new Error("Could not fetch document.")
1062 await open_document_from_buffer(await response.arrayBuffer(), path, path)
1063 } catch (error) {
1064 show_message(error.name + ": " + error.message)
1065 console.error(error)
1066 }
1067 }
1068
1069 function main() {
1070 clear_message()
1071 let params = new URLSearchParams(window.location.search)
1072 if (params.has("file"))
1073 open_document_from_url(params.get("file"))
1074 }
1075
1076 </script>