Treader-open/docs/epub-reader/mupdf-view-page.js

969 lines
27 KiB
JavaScript
Raw Permalink Normal View History

2026-02-10 19:01:19 +08:00
// Copyright (C) 2004-2022 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., 1305 Grant Avenue - Suite 200, Novato,
// CA 94945, U.S.A., +1(415)492-9861, for further information.
/* eslint-disable no-unused-vars */
class MupdfPageViewer {
constructor(worker, pageNumber, defaultSize, dpi, title) {
this.title = title
this.worker = worker
this.pageNumber = pageNumber
this.size = defaultSize
this.sizeIsDefault = true
const rootNode = document.createElement("div")
rootNode.classList.add("page")
const canvasNode = document.createElement("canvas")
rootNode.appendChild(canvasNode)
const anchor = document.createElement("a")
anchor.classList.add("anchor")
// TODO - document the "+ 1" better
anchor.id = "page" + (pageNumber + 1)
rootNode.appendChild(anchor)
rootNode.pageNumber = pageNumber
this.rootNode = rootNode
this.canvasNode = canvasNode
this.canvasCtx = canvasNode.getContext("2d")
this._updateSize(dpi)
this.renderPromise = null
this.queuedRenderArgs = null
this.textNode = null
this.textPromise = null
this.textResultObject = null
this.linksNode = null
this.linksPromise = null
this.linksResultObject = null
this.searchHitsNode = null
this.searchPromise = null
this.searchResultObject = null
this.lastSearchNeedle = null
this.searchNeedle = null
}
// TODO - move searchNeedle out
render(dpi, searchNeedle) {
// TODO - error handling
this._loadPageImg({ dpi })
this._loadPageText(dpi)
this._loadPageLinks(dpi)
this._loadPageSearch(dpi, searchNeedle)
}
// TODO - update child nodes
setZoom(zoomLevel) {
const dpi = ((zoomLevel * 96) / 100) | 0
this._updateSize(dpi)
}
setSearchNeedle(searchNeedle = null) {
this.searchNeedle = searchNeedle
}
clear() {
this.textNode?.remove()
this.linksNode?.remove()
this.searchHitsNode?.remove()
// TODO - use promise cancelling
this.renderPromise = null
this.textPromise = null
this.linksPromise = null
this.searchPromise = null
this.renderPromise = null
this.queuedRenderArgs = null
this.textNode = null
this.textPromise = null
this.textResultObject = null
this.linksNode = null
this.linksPromise = null
this.linksResultObject = null
this.searchHitsNode = null
this.searchPromise = null
this.searchResultObject = null
this.lastSearchNeedle = null
this.searchNeedle = null
this.mouseIsPressed = false
}
// TODO - this is destructive and makes other method get null ref errors
showError(functionName, error) {
console.error(`mupdf.${functionName}: ${error.message}:\n${error.stack}`)
let div = document.createElement("div")
div.classList.add("error")
div.textContent = error.name + ": " + error.message
//this.clear()
this.rootNode.replaceChildren(div)
}
async mouseDown(event, dpi) {
let { x, y } = this._getLocalCoords(event.clientX, event.clientY)
// TODO - remove "+ 1"
let changed = await this.worker.mouseDownOnPage(this.pageNumber + 1, dpi * devicePixelRatio, x, y)
this.mouseIsPressed = true
if (changed) {
this._invalidatePageImg()
this._loadPageImg({ dpi })
}
}
async mouseMove(event, dpi) {
let { x, y } = this._getLocalCoords(event.clientX, event.clientY)
let changed
// TODO - handle multiple buttons
// see https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
if (this.mouseIsPressed) {
if (event.buttons == 0) {
// In case we missed an onmouseup event outside of the frame
this.mouseIsPressed = false
// TODO - remove "+ 1"
changed = await this.worker.mouseUpOnPage(this.pageNumber + 1, dpi * devicePixelRatio, x, y)
} else {
// TODO - remove "+ 1"
changed = await this.worker.mouseDragOnPage(this.pageNumber + 1, dpi * devicePixelRatio, x, y)
}
} else {
// TODO - remove "+ 1"
changed = await this.worker.mouseMoveOnPage(this.pageNumber + 1, dpi * devicePixelRatio, x, y)
}
if (changed) {
this._invalidatePageImg()
this._loadPageImg({ dpi })
}
}
async mouseUp(event, dpi) {
let { x, y } = this._getLocalCoords(event.clientX, event.clientY)
this.mouseIsPressed = false
// TODO - remove "+ 1"
let changed = await this.worker.mouseUpOnPage(this.pageNumber + 1, dpi * devicePixelRatio, x, y)
if (changed) {
this._invalidatePageImg()
this._loadPageImg({ dpi })
}
}
// --- INTERNAL METHODS ---
// TODO - remove dpi param
_updateSize(dpi) {
// We use the `foo | 0` notation to convert dimensions to integers.
// This matches the conversion done in `mupdf.js` when `Pixmap.withBbox`
// calls `libmupdf._wasm_new_pixmap_with_bbox`.
this.rootNode.style.width = (((this.size.width * dpi) / 72) | 0) + "px"
this.rootNode.style.height = (((this.size.height * dpi) / 72) | 0) + "px"
this.canvasNode.style.width = (((this.size.width * dpi) / 72) | 0) + "px"
this.canvasNode.style.height = (((this.size.height * dpi) / 72) | 0) + "px"
}
async _loadPageImg(renderArgs) {
if (this.renderPromise != null || this.renderIsOngoing) {
// If a render is ongoing, we mark the current arguments as queued
// to be processed when the render ends.
// This also erases any previous queued render arguments.
this.queuedRenderArgs = renderArgs
return
}
if (this.canvasNode?.renderArgs != null) {
// If the current image node was rendered with the same arguments
// we skip the render.
if (renderArgs.dpi === this.canvasNode.renderArgs.dpi)
return
}
let { dpi } = renderArgs
try {
// FIXME - find better system for skipping duplicate renders
this.renderIsOngoing = true
if (this.sizeIsDefault) {
// TODO - remove "+ 1"
this.size = await this.worker.getPageSize(this.pageNumber + 1)
this.sizeIsDefault = false
this._updateSize(dpi)
}
// TODO - remove "+ 1"
this.renderPromise = this.worker.drawPageAsPixmap(this.pageNumber + 1, dpi * devicePixelRatio)
let imageData = await this.renderPromise
// if render was aborted, return early
if (imageData == null)
return
this.canvasNode.renderArgs = renderArgs
this.canvasNode.width = imageData.width
this.canvasNode.height = imageData.height
this.canvasCtx.putImageData(imageData, 0, 0)
this.canvasCtx.font = "2em serif"
this.canvasCtx.fillStyle = "#E0CACA";
this.canvasCtx.fillText(""+(this.pageNumber + 1), 1, 25);
} catch (error) {
this.showError("_loadPageImg", error)
} finally {
this.renderPromise = null
this.renderIsOngoing = false
}
if (this.queuedRenderArgs != null) {
// TODO - Error handling
this._loadPageImg(this.queuedRenderArgs)
this.queuedRenderArgs = null
}
}
_invalidatePageImg() {
if (this.canvasNode)
this.canvasNode.renderArgs = null
}
// TODO - replace "dpi" with "scale"?
async _loadPageText(dpi) {
// TODO - Disable text when editing (conditions to be figured out)
if (this.textNode != null && dpi === this.textNode.dpi) {
// Text was already rendered at the right scale, nothing to be done
return
}
if (this.textResultObject) {
// Text was already returned, just needs to be rescaled
this._applyPageText(this.textResultObject, dpi)
return
}
let textNode = document.createElement("div")
textNode.classList.add("text")
this.textNode?.remove()
this.textNode = textNode
this.rootNode.appendChild(textNode)
try {
// TODO - remove "+ 1"
this.textPromise = this.worker.getPageText(this.pageNumber + 1)
this.textResultObject = await this.textPromise
this._applyPageText(this.textResultObject, dpi)
} catch (error) {
this.showError("_loadPageText", error)
} finally {
this.textPromise = null
}
}
_applyPageText(textResultObject, dpi) {
this.textNode.dpi = dpi
let nodes = []
let pdf_w = []
let html_w = []
let text_len = []
let scale = dpi / 72
this.textNode.replaceChildren()
for (let block of textResultObject.blocks) {
if (block.type === "text") {
for (let line of block.lines) {
let text = document.createElement("span")
text.style.left = line.bbox.x * scale + "px"
text.style.top = (line.y - line.font.size * 0.8) * scale + "px"
text.style.height = line.bbox.h * 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.textContent = line.text
this.textNode.appendChild(text)
nodes.push(text)
pdf_w.push(line.bbox.w * scale)
text_len.push(line.text.length - 1)
}
}
}
for (let i = 0; i < nodes.length; ++i) {
if (text_len[i] > 0)
html_w[i] = nodes[i].clientWidth
}
for (let i = 0; i < nodes.length; ++i) {
if (text_len[i] > 0)
nodes[i].style.letterSpacing = (pdf_w[i] - html_w[i]) / text_len[i] + "px"
}
}
async _loadPageLinks(dpi) {
if (this.linksNode != null && dpi === this.linksNode.dpi) {
// Links were already rendered at the right scale, nothing to be done
return
}
if (this.linksResultObject) {
// Links were already returned, just need to be rescaled
this._applyPageLinks(this.linksResultObject, dpi)
return
}
let linksNode = document.createElement("div")
linksNode.classList.add("links")
// TODO - Figure out node order
this.linksNode?.remove()
this.linksNode = linksNode
this.rootNode.appendChild(linksNode)
try {
// TODO - remove "+ 1"
this.linksPromise = this.worker.getPageLinks(this.pageNumber + 1)
this.linksResultObject = await this.linksPromise
this._applyPageLinks(this.linksResultObject, dpi)
} catch (error) {
this.showError("_loadPageLinks", error)
} finally {
this.linksPromise = null
}
}
_applyPageLinks(linksResultObject, dpi) {
let scale = dpi / 72
this.linksNode.dpi = dpi
this.linksNode.replaceChildren()
for (let link of linksResultObject) {
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.linksNode.appendChild(a)
}
}
async _loadPageSearch(dpi, searchNeedle) {
if (
this.searchHitsNode != null &&
dpi === this.searchHitsNode.dpi &&
searchNeedle == this.searchHitsNode.searchNeedle
) {
// Search results were already rendered at the right scale, nothing to be done
return
}
if (this.searchResultObject && searchNeedle == this.searchHitsNode.searchNeedle) {
// Search results were already returned, just need to be rescaled
this._applyPageSearch(this.searchResultObject, dpi)
return
}
// TODO - cancel previous load
let searchHitsNode = document.createElement("div")
searchHitsNode.classList.add("searchHitList")
this.searchHitsNode?.remove()
this.searchHitsNode = searchHitsNode
this.rootNode.appendChild(searchHitsNode)
this.searchNeedle = searchNeedle ?? ""
try {
if (this.searchNeedle !== "") {
// TODO - remove "+ 1"
console.log("SEARCH", this.pageNumber + 1, JSON.stringify(this.searchNeedle))
this.searchPromise = this.worker.search(this.pageNumber + 1, this.searchNeedle)
this.searchResultObject = await this.searchPromise
} else {
this.searchResultObject = []
}
this._applyPageSearch(this.searchResultObject, searchNeedle, dpi)
} catch (error) {
this.showError("_loadPageSearch", error)
} finally {
this.searchPromise = null
}
}
_applyPageSearch(searchResultObject, searchNeedle, dpi) {
let scale = dpi / 72
this.searchHitsNode.searchNeedle = searchNeedle
this.searchHitsNode.dpi = dpi
this.searchHitsNode.replaceChildren()
for (let bbox of searchResultObject) {
let div = document.createElement("div")
div.classList.add("searchHit")
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.searchHitsNode.appendChild(div)
}
}
_getLocalCoords(clientX, clientY) {
const canvas = this.canvasNode
let x = clientX - canvas.getBoundingClientRect().left - canvas.clientLeft + canvas.scrollLeft
let y = clientY - canvas.getBoundingClientRect().top - canvas.clientTop + canvas.scrollTop
return { x, y }
}
}
let zoomLevels = [ 5,10, 25, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200 ]
// TODO - Split into separate file
class MupdfDocumentHandler {
constructor(documentUri, initialPage, showDefaultUi) {}
static async createHandler(mupdfWorker, viewerDivs) {
// TODO validate worker param
const handler = new MupdfDocumentHandler()
await mupdfWorker.layout(viewerDivs.documentWidth.value, viewerDivs.documentHeight.value, viewerDivs.documentFontSize.value)
const pageCount = await mupdfWorker.countPages()
const title = await mupdfWorker.documentTitle()
// Use second page as default page size (the cover page is often differently sized)
const defaultSize = await mupdfWorker.getPageSize(pageCount > 1 ? 2 : 1)
handler.mupdfWorker = mupdfWorker
handler.pageCount = pageCount
handler.title = title
handler.defaultSize = defaultSize
handler.searchNeedle = ""
handler.zoomLevel = 100
// TODO - Add a second observer with bigger margin to recycle old pages
handler.activePages = new Set()
handler.pageObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
handler.activePages.add(entry.target)
} else {
handler.activePages.delete(entry.target)
}
}
},
{
// This means we have roughly five viewports of vertical "head start" where
// the page is rendered before it becomes visible
rootMargin: "500% 0px",
}
)
// TODO
// This is a hack to compensate for the lack of a priority queue
// We wait until the user has stopped scrolling to load pages.
let scrollTimer = null
handler.scrollListener = function (event) {
if (scrollTimer !== null)
clearTimeout(scrollTimer)
scrollTimer = setTimeout(() => {
scrollTimer = null
handler._updateView()
}, 50)
}
document.addEventListener("scroll", handler.scrollListener)
//const rootDiv = document.createElement("div")
handler.gridMenubarDiv = viewerDivs.gridMenubarDiv
handler.gridSidebarDiv = viewerDivs.gridSidebarDiv
handler.gridMainDiv = viewerDivs.gridMainDiv
handler.pagesDiv = viewerDivs.pagesDiv
handler.searchDialogDiv = viewerDivs.searchDialogDiv
handler.outlineNode = viewerDivs.outlineNode
handler.searchStatusDiv = viewerDivs.searchStatusDiv
const pagesDiv = viewerDivs.pagesDiv
let pages = new Array(pageCount)
for (let i = 0; i < pageCount; ++i) {
const page = new MupdfPageViewer(mupdfWorker, i, defaultSize, handler._dpi(), handler.title)
pages[i] = page
pagesDiv.appendChild(page.rootNode)
handler.pageObserver.observe(page.rootNode)
}
function isPage(element) {
return element.tagName === "CANVAS" && element.closest("div.page") != null
}
const searchDivInput = document.createElement("input")
searchDivInput.id = "search-text"
searchDivInput.type = "search"
searchDivInput.size = 20
// Adjust size for mobile devices
if (window.innerWidth <= 480) {
searchDivInput.size = 10
}
searchDivInput.addEventListener("input", () => {
let newNeedle = searchDivInput.value ?? ""
handler.setSearch(newNeedle)
})
searchDivInput.addEventListener("keydown", (event) => {
if (event.key == "Enter")
handler.runSearch(event.shiftKey ? -1 : 1)
})
const searchStatusDiv = document.createElement("div")
searchStatusDiv.id = "search-status"
searchStatusDiv.innerText = "-"
// Create search dialog header
const searchHeader = document.createElement("div")
searchHeader.classList.add("dialog-header")
const searchTitle = document.createElement("h3")
searchTitle.classList.add("dialog-title")
searchTitle.textContent = "Search"
const dialogCloseButton = document.createElement("button")
dialogCloseButton.classList.add("sidebar-close")
dialogCloseButton.innerHTML = "×"
dialogCloseButton.title = "Close search"
dialogCloseButton.addEventListener("click", () => handler.hideSearchBox())
searchHeader.append(searchTitle, dialogCloseButton)
const searchFlex = document.createElement("div")
searchFlex.classList = [ "flex" ]
const ltButton = document.createElement("button")
ltButton.classList.add("sidebar-close")
ltButton.innerText = "<"
ltButton.title = "Previous search result"
ltButton.addEventListener("click", () => handler.runSearch(-1))
const gtButton = document.createElement("button")
gtButton.classList.add("sidebar-close")
gtButton.innerText = ">"
gtButton.title = "Next search result"
gtButton.addEventListener("click", () => handler.runSearch(1))
searchFlex.append(searchDivInput, ltButton, gtButton)
handler.searchDialogDiv.append(searchHeader, searchFlex, searchStatusDiv)
handler.searchStatusDiv = searchStatusDiv
handler.searchDivInput = searchDivInput
handler.currentSearchPage = 1
// TODO use rootDiv instead
pagesDiv.addEventListener(
"wheel",
(event) => {
if (event.ctrlKey || event.metaKey) {
if (event.deltaY < 0)
handler.zoomIn()
else if (event.deltaY > 0)
handler.zoomOut()
event.preventDefault()
}
},
{ passive: false }
)
//handler.rootDiv = rootDiv
handler.pagesDiv = pagesDiv // TODO - rename
handler.pages = pages
// TODO - remove await
let outline = await mupdfWorker.documentOutline()
let outlineNode = viewerDivs.outlineNode
if (outline) {
handler._buildOutline(outlineNode, outline)
//handler.showOutline()
} else {
handler.hideOutline()
}
// TODO - remove once we add a priority queue
for (let i = 0; i < Math.min(pageCount, 5); ++i) {
handler.activePages.add(pages[i].rootNode)
}
handler._updateView()
return handler
}
_updateView() {
const dpi = this._dpi()
for (const page of this.activePages) {
this.pages[page.pageNumber].render(dpi, this.searchNeedle)
}
}
// TODO - remove?
_dpi() {
return ((this.zoomLevel * 96) / 100) | 0
}
goToPage(pageNumber) {
pageNumber = Math.max(0, Math.min(pageNumber, this.pages.length - 1))
this.pages[pageNumber].rootNode.scrollIntoView()
}
zoomIn() {
// TODO - instead find next larger zoom
let curr = zoomLevels.indexOf(this.zoomLevel)
let next = zoomLevels[curr + 1]
if (next)
this.setZoom(next)
}
zoomOut() {
let curr = zoomLevels.indexOf(this.zoomLevel)
let next = zoomLevels[curr - 1]
if (next)
this.setZoom(next)
}
setZoom(newZoom) {
if (this.zoomLevel === newZoom)
return
this.zoomLevel = newZoom
for (const page of this.pages) {
page.setZoom(newZoom)
}
this._updateView()
}
clearSearch() {
// TODO
}
setSearch(newNeedle) {
this.searchStatusDiv.textContent = ""
if (this.searchNeedle !== newNeedle) {
this.searchNeedle = newNeedle
this._updateView()
}
}
showSearchBox() {
// TODO - Fix what happens when you re-open search with existing text
this.searchDialogDiv.style.display = "block"
this.searchDivInput.focus()
this.searchDivInput.select()
this.setSearch(this.searchDivInput.value ?? "")
}
hideSearchBox() {
this.searchStatusDiv.textContent = ""
this.searchDialogDiv.style.display = "none"
this.cancelSearch()
this.setSearch("")
}
async runSearch(direction) {
let searchStatusDiv = this.searchStatusDiv
try {
let page = this.currentSearchPage + direction
while (page >= 1 && page < this.pageCount) {
// We run the check once per loop iteration,
// in case the search was cancel during the 'await' below.
if (this.searchNeedle === "") {
searchStatusDiv.textContent = ""
return
}
searchStatusDiv.textContent = `Searching page ${page}.`
await this.pages[page]._loadPageSearch(this._dpi(), this.searchNeedle)
const hits = this.pages[page].searchResultObject ?? []
if (hits.length > 0) {
this.pages[page].rootNode.scrollIntoView()
this.currentSearchPage = page
searchStatusDiv.textContent = `${hits.length} hits on page ${page}.`
return
}
page += direction
}
searchStatusDiv.textContent = "No more search hits."
} catch (error) {
console.error(`mupdf.runSearch: ${error.message}:\n${error.stack}`)
}
}
cancelSearch() {
// TODO
}
showOutline() {
this.gridSidebarDiv.style.display = "block"
this.gridMainDiv.classList.replace("sidebarHidden", "sidebarVisible")
}
hideOutline() {
this.gridSidebarDiv.style.display = "none"
this.gridMainDiv.classList.replace("sidebarVisible", "sidebarHidden")
}
toggleOutline() {
let node = this.gridSidebarDiv
if (node.style.display === "none" || node.style.display === "")
this.showOutline()
else
this.hideOutline()
}
_buildOutline(listNode, outline) {
for (let item of outline) {
let itemNode = document.createElement("li")
let aNode = document.createElement("a")
// TODO - document the "+ 1" better
aNode.href = `#page${item.page + 1}`
//aNode.href = `#page${item.page}`
//aNode.href = `#page${item.uri}`
//aNode.href = JSON.stringify(item)
//aNode.textContent = JSON.stringify(item)
aNode.textContent = item.title
itemNode.appendChild(aNode)
listNode.appendChild(itemNode)
if (item.down) {
itemNode = document.createElement("ul")
this._buildOutline(itemNode, item.down)
listNode.appendChild(itemNode)
}
}
}
clear() {
document.removeEventListener("scroll", this.scrollListener)
this.pagesDiv?.replaceChildren()
this.outlineNode?.replaceChildren()
this.searchDialogDiv?.replaceChildren()
for (let page of this.pages ?? []) {
page.clear()
}
this.pageObserver?.disconnect()
this.cancelSearch()
}
}
// TODO - Split into separate file
class MupdfDocumentViewer {
constructor(mupdfWorker) {
this.mupdfWorker = mupdfWorker
this.documentHandler = null
this.placeholderDiv = document.getElementById("placeholder")
this.viewerDivs = {
gridMenubarDiv: document.getElementById("grid-menubar"),
gridSidebarDiv: document.getElementById("grid-sidebar"),
gridMainDiv: document.getElementById("grid-main"),
pagesDiv: document.getElementById("pages"),
searchDialogDiv: document.getElementById("search-dialog"),
outlineNode: document.getElementById("outline"),
searchStatusDiv: document.getElementById("search-status"),
documentFontSize: document.getElementById("document-font-size"),
documentWidth: document.getElementById("document-width"),
documentHeight: document.getElementById("document-height"),
}
}
async openFile(file) {
try {
if (!(file instanceof File)) {
throw new Error(`Argument '${file}' is not a file`)
}
history.replaceState(null, null, window.location.pathname)
this.clear()
let loadingText = document.createElement("div")
loadingText.textContent = "Loading document..."
this.placeholderDiv.replaceChildren(loadingText)
await this.mupdfWorker.openDocumentFromBuffer(await file.arrayBuffer(), file.name)
await this._initDocument(file.name)
} catch (error) {
this.showDocumentError("openFile", error)
}
}
async openURL(url, progressive, prefetch) {
try {
this.clear()
let loadingText = document.createElement("div")
loadingText.textContent = "Loading document..."
this.placeholderDiv.replaceChildren(loadingText)
let headResponse = await fetch(url, { method: "HEAD" })
if (!headResponse.ok)
throw new Error("Could not fetch document.")
let acceptRanges = headResponse.headers.get("Accept-Ranges")
let contentLength = headResponse.headers.get("Content-Length")
let contentType = headResponse.headers.get("Content-Type")
// TODO - Log less stuff
console.log("HEAD", url)
console.log("Content-Length", contentLength)
console.log("Content-Type", contentType)
if (acceptRanges === "bytes" && progressive) {
console.log("USING HTTP RANGE REQUESTS")
await mupdfView.openDocumentFromUrl(url, contentLength, progressive, prefetch, contentType || url)
} else {
let bodyResponse = await fetch(url)
if (!bodyResponse.ok)
throw new Error("Could not fetch document.")
let buffer = await bodyResponse.arrayBuffer()
await mupdfView.openDocumentFromBuffer(buffer, contentType || url)
}
await this._initDocument(url)
} catch (error) {
this.showDocumentError("openURL", error)
}
}
openEmpty() {
this.clear()
this.placeholderDiv.replaceChildren()
// TODO - add "empty" placeholder
// add drag-and-drop support?
}
async _initDocument(docName) {
this.documentHandler = await MupdfDocumentHandler.createHandler(this.mupdfWorker, this.viewerDivs)
this.placeholderDiv.replaceChildren()
console.log("mupdf: Loaded", JSON.stringify(docName), "with", this.documentHandler.pageCount, "pages.")
// Change tab title
//document.title = this.documentHandler.title || docName
//document.getElementById("document-title").textContent = docName
}
showDocumentError(functionName, error) {
console.error(`mupdf.${functionName}: ${error.message}:\n${error.stack}`)
let errorDiv = document.createElement("div")
errorDiv.classList.add("error")
errorDiv.textContent = error.name + ": " + error.message
this.clear()
this.placeholderDiv.replaceChildren(errorDiv)
}
goToPage(pageNumber) {
this.documentHandler?.goToPage(pageNumber)
}
toggleFullscreen() {
if (!document.fullscreenElement) {
this.enterFullscreen()
} else {
this.exitFullscreen()
}
}
enterFullscreen() {
document.documentElement.requestFullscreen().catch((err) => {
console.error("Could not enter fullscreen mode:", err)
})
}
exitFullscreen() {
document.exitFullscreen()
}
zoomIn() {
this.documentHandler?.zoomIn()
}
zoomOut() {
this.documentHandler?.zoomOut()
}
setZoom(newZoom) {
this.documentHandler?.setZoom(newZoom)
}
clearSearch() {
this.documentHandler?.clearSearch()
}
setSearch(newNeedle) {
this.documentHandler?.setSearch(newNeedle)
}
showSearchBox() {
this.documentHandler?.showSearchBox()
}
hideSearchBox() {
this.documentHandler?.hideSearchBox()
}
runSearch(direction) {
this.documentHandler?.runSearch(direction)
}
cancelSearch() {
this.documentHandler?.cancelSearch()
}
showOutline() {
this.documentHandler?.showOutline()
}
hideOutline() {
this.documentHandler?.hideOutline()
}
toggleOutline() {
this.documentHandler?.toggleOutline()
}
clear() {
this.documentHandler?.clear()
// TODO
}
}