index.html 24 KB

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