| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076 |
- <!DOCTYPE html>
- <!--
- Copyright (C) 2022, 2024, 2025 Artifex Software, Inc.
- This file is part of MuPDF.
- MuPDF is free software: you can redistribute it and/or modify it under the
- terms of the GNU Affero General Public License as published by the Free
- Software Foundation, either version 3 of the License, or (at your option)
- any later version.
- MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
- details.
- You should have received a copy of the GNU Affero General Public License
- along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
- Alternative licensing terms are available from the licensor.
- For commercial licensing, see <https://www.artifex.com/> or contact
- Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
- CA 94129, USA, for further information.
- -->
- <title>MuPDF.js</title>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
- <link rel="shortcut icon" href="favicon.svg">
- <style>
- * {
- box-sizing: border-box;
- }
- /* APPEARANCE */
- html {
- font-family: sans-serif;
- font-size: 18px;
- background-color: gray;
- }
- header {
- border-bottom: 1px solid black;
- background-color: gainsboro;
- }
- footer {
- border-top: 1px solid black;
- background-color: gainsboro;
- }
- aside {
- background-color: white;
- border-right: 1px solid black;
- }
- #message {
- text-align: center;
- font-size: 24pt;
- font-weight: bold;
- color: silver;
- }
- details[open] > summary {
- background-color: #0004;
- }
- menu {
- min-width: 140px;
- border: 1px solid black;
- background-color: white;
- color: black;
- }
- menu li:hover {
- background-color: black;
- color: white;
- }
- /* LAYOUT */
- html {
- margin: 0;
- padding: 0;
- width: 100%;
- height: 100%;
- }
- body {
- margin: 0;
- padding: 0;
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- grid-template-rows: auto minmax(0, 1fr) auto;
- width: 100%;
- height: 100%;
- overflow: clip;
- }
- header {
- position: relative;
- user-select: none;
- grid-column: 1/3;
- grid-row: 1;
- display: flex;
- flex-wrap: wrap;
- }
- footer {
- grid-column: 1/3;
- grid-row: 3;
- display: flex;
- padding: 8px;
- gap: 8px;
- }
- aside {
- grid-column: 1;
- grid-row: 2;
- overflow-y: auto;
- width: 250px;
- }
- main {
- grid-column: 2;
- grid-row: 2;
- overflow: scroll;
- }
- summary {
- padding: 4px 8px;
- cursor: pointer;
- list-style: none;
- }
- /* workaround for bug in Safari details appearance */
- summary::-webkit-details-marker {
- display: none;
- }
- menu {
- position: absolute;
- overflow-y: auto;
- margin: 0;
- padding: 0;
- list-style: none;
- z-index: 500;
- }
- menu li {
- padding: 4px 8px;
- cursor: pointer;
- }
- /* OUTLINE */
- #outline {
- font-size: 12px;
- }
- #outline ul {
- margin: 0;
- padding-left: 20px;
- }
- #outline a {
- color: black;
- text-decoration: none;
- }
- #outline a:hover {
- color: blue;
- text-decoration: underline;
- }
- /* PAGES */
- #pages {
- margin: 0 auto;
- }
- div.page {
- position: relative;
- background-color: white;
- margin: 16px auto;
- box-shadow: 0px 2px 8px #0004;
- }
- div.page * {
- position: absolute;
- }
- div.page canvas {
- user-select: none;
- }
- svg.text {
- width: 100%;
- height: 100%;
- }
- svg.text text {
- white-space: pre;
- line-height: 1;
- fill: transparent;
- }
- svg.text ::selection {
- background: hsla(220, 100%, 50%, 0.2);
- color: transparent;
- }
- div.link a:hover {
- border: 1px dotted blue;
- }
- #pages.do-content-select div.link {
- pointer-events: none;
- }
- div.search > div {
- pointer-events: none;
- border: 1px solid hotpink;
- background-color: lightpink;
- mix-blend-mode: multiply;
- }
- </style>
- <body>
- <header id="menubar-panel">
- <details>
- <summary>File</summary>
- <menu>
- <li onclick="document.getElementById('open-file-input').click()">Open File...
- </menu>
- </details>
- <details>
- <summary>Edit</summary>
- <menu>
- <li onclick="show_search_panel()">Search...
- </menu>
- </details>
- <details>
- <summary>View</summary>
- <menu>
- <li onclick="toggle_fullscreen()">Fullscreen
- <li onclick="toggle_outline_panel()">Outline
- <li onclick="zoom_to(48)">50%
- <li onclick="zoom_to(72)">75% (72 dpi)
- <li onclick="zoom_to(96)">100% (96 dpi)
- <li onclick="zoom_to(120)">125%
- <li onclick="zoom_to(144)">150%
- <li onclick="zoom_to(192)">200%
- </menu>
- </details>
- </header>
- <aside id="outline-panel" style="display:none">
- <ul id="outline">
- <!-- outline inserted here -->
- </ul>
- </aside>
- <main id="page-panel">
- <div id="message">
- Loading MuPDF.js...
- </div>
- <div id="pages">
- <!-- pages inserted here -->
- </div>
- </main>
- <footer id="search-panel" style="display:none">
- <input
- id="search-input"
- type="search"
- size="40"
- placeholder="Search..."
- >
- <button id="search-prev" onclick="run_search(-1, 1)"><</button>
- <button id="search-next" onclick="run_search(1, 1)">></button>
- <div id="search-status" style="flex-grow:1"></div>
- <button onclick="hide_search_panel()">X</button>
- </footer>
- <!-- hidden input for file dialog -->
- <input
- style="display: none"
- id="open-file-input"
- type="file"
- accept=".pdf,application/pdf"
- onchange="open_document_from_file(event.target.files[0])"
- >
- </body>
- <script>
- "use strict"
- // FAST SORTED ARRAY FUNCTIONS
- function array_remove(array, index) {
- let n = array.length
- for (let i = index + 1; i < n; ++i)
- array[i - 1] = array[i]
- array.length = n - 1
- }
- function array_insert(array, index, item) {
- for (let i = array.length; i > index; --i)
- array[i] = array[i - 1]
- array[index] = item
- }
- function set_has(set, item) {
- let a = 0
- let b = set.length - 1
- while (a <= b) {
- let m = (a + b) >> 1
- let x = set[m]
- if (item < x)
- b = m - 1
- else if (item > x)
- a = m + 1
- else
- return true
- }
- return false
- }
- function set_add(set, item) {
- let a = 0
- let b = set.length - 1
- while (a <= b) {
- let m = (a + b) >> 1
- let x = set[m]
- if (item < x)
- b = m - 1
- else if (item > x)
- a = m + 1
- else
- return
- }
- array_insert(set, a, item)
- }
- function set_delete(set, item) {
- let a = 0
- let b = set.length - 1
- while (a <= b) {
- let m = (a + b) >> 1
- let x = set[m]
- if (item < x)
- b = m - 1
- else if (item > x)
- a = m + 1
- else {
- array_remove(set, m)
- return
- }
- }
- }
- // LOADING AND ERROR MESSAGES
- function show_message(msg) {
- document.getElementById("message").textContent = msg
- }
- function clear_message() {
- document.getElementById("message").textContent = ""
- }
- // MENU BAR
- function close_all_menus(self) {
- for (let node of document.querySelectorAll("header > details"))
- if (node !== self)
- node.removeAttribute("open")
- }
- /* close menu if opening another */
- for (let node of document.querySelectorAll("header > details")) {
- node.addEventListener("click", function () {
- close_all_menus(node)
- })
- }
- /* close menu after selecting something */
- for (let node of document.querySelectorAll("header > details > menu")) {
- node.addEventListener("click", function () {
- close_all_menus(null)
- })
- }
- /* click anywhere outside the menu to close it */
- window.addEventListener("mousedown", function (evt) {
- let e = evt.target
- while (e) {
- if (e.tagName === "DETAILS")
- return
- e = e.parentElement
- }
- close_all_menus(null)
- })
- /* close menus if window loses focus */
- window.addEventListener("blur", function () {
- close_all_menus(null)
- })
- // BACKGROUND WORKER
- const worker = new Worker("worker.js", { type: "module" })
- worker._promise_id = 1
- worker._promise_map = new Map()
- worker.wrap = function (name) {
- return function (...args) {
- return new Promise(function (resolve, reject) {
- let id = worker._promise_id++
- worker._promise_map.set(id, { resolve, reject })
- if (args[0] instanceof ArrayBuffer)
- worker.postMessage([ name, id, args ], [ args[0] ])
- else
- worker.postMessage([ name, id, args ])
- })
- }
- }
- worker.onmessage = function (event) {
- let [ type, id, result ] = event.data
- let error
- switch (type) {
- case "INIT":
- for (let method of result)
- worker[method] = worker.wrap(method)
- main()
- break
- case "RESULT":
- worker._promise_map.get(id).resolve(result)
- worker._promise_map.delete(id)
- break
- case "ERROR":
- error = new Error(result.message)
- error.name = result.name
- error.stack = result.stack
- worker._promise_map.get(id).reject(error)
- worker._promise_map.delete(id)
- break
- default:
- error = new Error(`Invalid message: ${type}`)
- worker._promise_map.get(id).reject(error)
- break
- }
- }
- // PAGE VIEW
- class PageView {
- constructor(doc, pageNumber, defaultSize, zoom) {
- this.doc = doc
- this.pageNumber = pageNumber // 0-based
- this.size = defaultSize
- this.loadPromise = false
- this.drawPromise = false
- this.rootNode = document.createElement("div")
- this.rootNode.id = "page" + (pageNumber + 1)
- this.rootNode.className = "page"
- this.rootNode.page = this
- this.canvasNode = document.createElement("canvas")
- this.canvasCtx = this.canvasNode.getContext("2d")
- this.rootNode.appendChild(this.canvasNode)
- this.textData = null
- this.textNode = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- this.textNode.classList.add("text")
- this.rootNode.appendChild(this.textNode)
- this.linkData = null
- this.linkNode = document.createElement("div")
- this.linkNode.className = "link"
- this.rootNode.appendChild(this.linkNode)
- this.needle = null
- this.loadNeedle = null
- this.showNeedle = null
- this.searchData = null
- this.searchNode = document.createElement("div")
- this.searchNode.className = "search"
- this.rootNode.appendChild(this.searchNode)
- this.zoom = zoom
- this._updateSize()
- }
- // Update page element size for current zoom level.
- _updateSize() {
- // We Math.ceil to match the behavior of fz_irect_from_rect that is used by the worker.
- this.rootNode.style.width = Math.ceil((this.size.width * this.zoom) / 72) + "px"
- this.rootNode.style.height = Math.ceil((this.size.height * this.zoom) / 72) + "px"
- this.canvasNode.style.width = Math.ceil((this.size.width * this.zoom) / 72) + "px"
- this.canvasNode.style.height = Math.ceil((this.size.height * this.zoom) / 72) + "px"
- }
- setZoom(zoom) {
- if (this.zoom !== zoom) {
- this.zoom = zoom
- this._updateSize()
- }
- }
- setSearch(needle) {
- if (this.needle !== needle)
- this.needle = needle
- }
- async _load() {
- console.log("LOADING", this.pageNumber)
- this.size = await worker.getPageSize(this.doc, this.pageNumber)
- this.textData = await worker.getPageText(this.doc, this.pageNumber)
- this.linkData = await worker.getPageLinks(this.doc, this.pageNumber)
- this._updateSize()
- }
- async _loadSearch() {
- if (this.loadNeedle !== this.needle) {
- this.loadNeedle = this.needle
- if (!this.needle)
- this.searchData = null
- else
- this.searchData = await worker.search(this.doc, this.pageNumber, this.needle)
- }
- }
- async _show() {
- if (!this.loadPromise)
- this.loadPromise = this._load()
- await this.loadPromise
- // Render image if zoom factor has changed!
- if (this.canvasNode.zoom !== this.zoom)
- this._render()
- // (Re-)create HTML nodes if zoom factor has changed
- if (this.textNode.zoom !== this.zoom)
- this._showText()
- // (Re-)create HTML nodes if zoom factor has changed
- if (this.linkNode.zoom !== this.zoom)
- this._showLinks()
- // Reload search hits if the needle has changed.
- // TODO: race condition with multiple queued searches
- if (this.loadNeedle !== this.needle)
- await this._loadSearch()
- // (Re-)create HTML nodes if search changed or zoom factor changed
- if (this.showNeedle !== this.needle || this.searchNode.zoom !== this.zoom)
- this._showSearch()
- }
- async _render() {
- // Remember zoom value when we start rendering.
- let zoom = this.zoom
- // If the current image node was rendered with the same arguments we skip the render.
- if (this.canvasNode.zoom === this.zoom)
- return
- if (this.drawPromise) {
- // If a render is ongoing, don't queue a new render immediately!
- // When the on-going render finishes, we check the page zoom value.
- // If it is stale, we immediately queue a new render.
- console.log("BUSY DRAWING", this.pageNumber)
- return
- }
- console.log("DRAWING", this.pageNumber, zoom)
- this.canvasNode.zoom = this.zoom
- this.drawPromise = worker.drawPageAsPixmap(this.doc, this.pageNumber, zoom * devicePixelRatio)
- let imageData = await this.drawPromise
- if (imageData == null)
- return
- this.drawPromise = null
- if (this.zoom === zoom) {
- // Render is still valid. Use it!
- console.log("FRESH IMAGE", this.pageNumber)
- this.canvasNode.width = imageData.width
- this.canvasNode.height = imageData.height
- this.canvasCtx.putImageData(imageData, 0, 0)
- } else {
- // Uh-oh. This render is already stale. Try again!
- console.log("STALE IMAGE", this.pageNumber)
- if (set_has(page_visible, this.pageNumber))
- this._render()
- }
- }
- _showText() {
- let frag = document.createDocumentFragment()
- let scale = this.zoom / 72
- for (let block of this.textData.blocks) {
- if (block.type === "text") {
- for (let line of block.lines) {
- let text = document.createElementNS("http://www.w3.org/2000/svg", "text")
- text.setAttribute("x", line.bbox.x * scale + "px")
- text.setAttribute("y", line.y * scale + "px")
- text.style.fontSize = line.font.size * scale + "px"
- text.style.fontFamily = line.font.family
- text.style.fontWeight = line.font.weight
- text.style.fontStyle = line.font.style
- text.setAttribute("textLength", line.bbox.w * scale + "px")
- text.setAttribute("lengthAdjust", "spacingAndGlyphs")
- text.textContent = line.text
- frag.appendChild(text)
- }
- }
- }
- this.textNode.zoom = this.zoom
- this.textNode.replaceChildren(frag)
- }
- _showLinks() {
- this.linkNode.zoom = this.zoom
- this.linkNode.replaceChildren()
- let scale = this.zoom / 72
- for (let link of this.linkData) {
- let a = document.createElement("a")
- a.href = link.href
- a.style.left = link.x * scale + "px"
- a.style.top = link.y * scale + "px"
- a.style.width = link.w * scale + "px"
- a.style.height = link.h * scale + "px"
- this.linkNode.appendChild(a)
- }
- }
- _showSearch() {
- this.showNeedle = this.needle
- this.searchNode.zoom = this.zoom
- this.searchNode.replaceChildren()
- if (this.searchData) {
- let scale = this.zoom / 72
- for (let bbox of this.searchData) {
- let div = document.createElement("div")
- div.style.left = bbox.x * scale + "px"
- div.style.top = bbox.y * scale + "px"
- div.style.width = bbox.w * scale + "px"
- div.style.height = bbox.h * scale + "px"
- this.searchNode.appendChild(div)
- }
- }
- }
- }
- // DOCUMENT VIEW
- var current_doc = 0
- var current_zoom = 96
- var page_list = null // all pages in document
- // Track page visibility as the user scrolls through the document.
- // When a page comes near the viewport, we add it to the list of
- // "visible" pages and queue up rendering it.
- var page_visible = []
- var page_observer = new IntersectionObserver(
- function (entries) {
- for (let entry of entries) {
- let page = entry.target.page
- if (entry.isIntersecting)
- set_add(page_visible, page.pageNumber)
- else
- set_delete(page_visible, page.pageNumber)
- }
- queue_update_view()
- },
- {
- // This means we have 3 viewports of vertical "head start" where
- // the page is rendered before it becomes visible.
- root: document.getElementById("page-panel"),
- rootMargin: "25% 0px 300% 0px",
- }
- )
- // Timer that waits until things settle before kicking off rendering.
- var update_view_timer = 0
- function queue_update_view() {
- if (update_view_timer)
- clearTimeout(update_view_timer)
- update_view_timer = setTimeout(update_view, 50)
- }
- function update_view() {
- if (update_view_timer)
- clearTimeout(update_view_timer)
- update_view_timer = 0
- for (let i of page_visible)
- page_list[i]._show()
- }
- function find_visible_page() {
- let panel = document.getElementById("page-panel").getBoundingClientRect()
- let panel_mid = (panel.top + panel.bottom) / 2
- for (let p of page_visible) {
- let rect = page_list[p].rootNode.getBoundingClientRect()
- if (rect.top <= panel_mid && rect.bottom >= panel_mid)
- return p
- }
- return page_visible[0]
- }
- function zoom_in() {
- zoom_to(Math.min(current_zoom + 12, 384))
- }
- function zoom_out() {
- zoom_to(Math.max(current_zoom - 12, 48))
- }
- function zoom_to(new_zoom) {
- if (current_zoom === new_zoom)
- return
- current_zoom = new_zoom
- // TODO: keep page coord at center of cursor in place when zooming
- let p = find_visible_page()
- for (let page of page_list)
- page.setZoom(current_zoom)
- page_list[p].rootNode.scrollIntoView()
- queue_update_view()
- }
- // KEY BINDINGS & MOUSE WHEEL ZOOM
- window.addEventListener("wheel",
- function (event) {
- // Intercept Ctl+MOUSEWHEEL that change browser zoom.
- // Our page rendering requires a 1-to-1 pixel scale.
- if (event.ctrlKey || event.metaKey) {
- if (event.deltaY < 0)
- zoom_in()
- else if (event.deltaY > 0)
- zoom_out()
- event.preventDefault()
- }
- },
- { passive: false }
- )
- window.addEventListener("keydown", function (event) {
- // Intercept and override some keyboard shortcuts.
- // We must override the Ctl-PLUS and Ctl-MINUS shortcuts that change browser zoom.
- // Our page rendering requires a 1-to-1 pixel scale.
- if (event.ctrlKey || event.metaKey) {
- switch (event.keyCode) {
- // '=' / '+' on various keyboards
- case 61:
- case 107:
- case 187:
- case 171:
- zoom_in()
- event.preventDefault()
- break
- // '-'
- case 173:
- case 109:
- case 189:
- zoom_out()
- event.preventDefault()
- break
- // '0'
- case 48:
- case 96:
- zoom_to(100)
- break
- // 'A'
- case 65:
- // Ctrl-A full selection
- document.getSelection().selectAllChildren(document.getElementById("pages"))
- event.preventDefault()
- break
- // 'F'
- case 70:
- show_search_panel()
- event.preventDefault()
- break
- // 'G'
- case 71:
- show_search_panel()
- run_search(event.shiftKey ? -1 : 1, 1)
- event.preventDefault()
- break
- }
- }
- if (event.key === "Escape") {
- hide_search_panel()
- }
- })
- function toggle_fullscreen() {
- // Safari on iPhone doesn't support Fullscreen
- if (typeof document.documentElement.requestFullscreen !== "function")
- return
- if (document.fullscreenElement)
- document.exitFullscreen()
- else
- document.documentElement.requestFullscreen()
- }
- // Mark TEXT-SELECTION State
- function remove_selection_state(e) {
- document.getElementById("pages").classList.remove("do-content-select")
- document.removeEventListener("mouseup", remove_selection_state)
- }
- document.addEventListener("selectstart", function (event) {
- document.getElementById("pages").classList.add("do-content-select")
- document.addEventListener("mouseup", remove_selection_state)
- })
- // SEARCH
- let search_panel = document.getElementById("search-panel")
- let search_status = document.getElementById("search-status")
- let search_input = document.getElementById("search-input")
- var current_search_needle = ""
- var last_search_page = -1
- search_input.onchange = function (event) {
- last_search_page = -1
- }
- search_input.onkeyup = function (event) {
- if (event.key === 'Enter') {
- if (event.shiftKey)
- document.getElementById("search-prev").click()
- else
- document.getElementById("search-next").click()
- }
- }
- function show_search_panel() {
- if (!page_list)
- return
- search_panel.style.display = ""
- search_input.focus()
- search_input.select()
- }
- function hide_search_panel() {
- search_panel.style.display = "none"
- search_input.value = ""
- set_search_needle("")
- }
- function set_search_needle(needle) {
- search_status.textContent = ""
- current_search_needle = needle
- if (!page_list)
- return
- for (let page of page_list)
- page.setSearch(current_search_needle)
- queue_update_view()
- }
- async function run_search(direction, step) {
- // start search from visible page
- set_search_needle(search_input.value)
- let page = 0;
- if (last_search_page === -1)
- page = find_visible_page()
- else {
- page = last_search_page
- if (step)
- page += direction
- }
- while (page >= 0 && page < page_list.length) {
- // We run the check once per loop iteration,
- // in case the search was cancel during the 'await' below.
- if (current_search_needle === "") {
- search_status.textContent = ""
- return
- }
- search_status.textContent = `Searching page ${page + 1}.`
- if (page_list[page].loadNeedle !== page_list[page].needle)
- await page_list[page]._loadSearch()
- const hits = page_list[page].searchData
- if (hits && hits.length > 0) {
- page_list[page].rootNode.scrollIntoView()
- last_search_page = page
- const word = hits.length === 1 ? "hit" : "hits"
- search_status.textContent = `${hits.length} ${word} on page ${page + 1}.`
- return
- }
- page += direction
- }
- search_status.textContent = "No more search hits."
- }
- // OUTLINE
- function build_outline(parent, outline) {
- for (let item of outline) {
- let node = document.createElement("li")
- let a = document.createElement("a")
- a.href = "#page" + (item.page + 1)
- a.textContent = item.title
- node.appendChild(a)
- if (item.down) {
- let down = document.createElement("ul")
- build_outline(down, item.down)
- node.appendChild(down)
- }
- parent.appendChild(node)
- }
- }
- function toggle_outline_panel() {
- if (document.getElementById("outline-panel").style.display === "none")
- show_outline_panel()
- else
- hide_outline_panel()
- }
- function show_outline_panel() {
- if (!page_list)
- return
- document.getElementById("outline-panel").style.display = "block"
- }
- function hide_outline_panel() {
- document.getElementById("outline-panel").style.display = "none"
- }
- // DOCUMENT LOADING
- function close_document() {
- clear_message()
- hide_outline_panel()
- hide_search_panel()
- if (current_doc) {
- worker.closeDocument(current_doc)
- current_doc = 0
- document.getElementById("outline").replaceChildren()
- document.getElementById("pages").replaceChildren()
- for (let page of page_list)
- page_observer.unobserve(page.rootNode)
- page_visible.length = 0
- }
- page_list = null
- }
- async function init_document(title) {
- document.title = await worker.documentTitle(current_doc) || title
- var page_count = await worker.countPages(current_doc)
- // Use second page as default page size (the cover page is often differently sized)
- var page_size = await worker.getPageSize(current_doc, page_count > 1 ? 1 : 0)
- page_list = []
- for (let i = 0; i < page_count; ++i)
- page_list[i] = new PageView(current_doc, i, page_size, current_zoom)
- for (let page of page_list) {
- document.getElementById("pages").appendChild(page.rootNode)
- page_observer.observe(page.rootNode)
- }
- var outline = await worker.documentOutline(current_doc)
- if (outline) {
- build_outline(document.getElementById("outline"), outline)
- show_outline_panel()
- } else {
- hide_outline_panel()
- }
- clear_message()
- current_search_needle = ""
- last_search_page = -1
- }
- async function open_document_from_buffer(buffer, magic, title) {
- current_doc = await worker.openDocumentFromBuffer(buffer, magic)
- await init_document(title)
- }
- async function open_document_from_blob(blob, magic, title) {
- current_doc = await worker.openDocumentFromBlob(blob, magic)
- await init_document(title)
- }
- async function open_document_from_file(file) {
- close_document()
- try {
- show_message("Opening " + file.name)
- history.replaceState(null, null, window.location.pathname)
- await open_document_from_blob(file, file.name, file.name)
- } catch (error) {
- show_message(error.name + ": " + error.message)
- console.error(error)
- }
- }
- async function open_document_from_url(path) {
- close_document()
- try {
- show_message("Loading " + path)
- let response = await fetch(path)
- if (!response.ok)
- throw new Error("Could not fetch document.")
- await open_document_from_buffer(await response.arrayBuffer(), path, path)
- } catch (error) {
- show_message(error.name + ": " + error.message)
- console.error(error)
- }
- }
- function main() {
- clear_message()
- let params = new URLSearchParams(window.location.search)
- if (params.has("file"))
- open_document_from_url(params.get("file"))
- }
- </script>
|