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

969 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 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
}
}