Gast commit!

This commit is contained in:
2026-02-10 19:01:19 +08:00
commit 345f628a8a
3074 changed files with 278037 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
mupdf-
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
VERSION_TAG=1.23.11
MUPDF_DIR=mupdf-$VERSION_TAG
EMSDK_DIR=/Volumes/SSD-USB/git2/emsdk
git clone --recursive git://git.ghostscript.com/mupdf.git --branch $VERSION_TAG $MUPDF_DIR
MUPDF_OPTS="-Os -D_TOFU -DTOFU_CJK -DFZ_ENABLE_XPS=1 -DFZ_ENABLE_SVG=1 -DFZ_ENABLE_CBZ=1 -DFZ_ENABLE_IMG=1 -DFZ_ENABLE_HTML=1 -DFZ_ENABLE_EPUB=1 -DFZ_ENABLE_JS=0 -DFZ_ENABLE_OCR_OUTPUT=0 -DFZ_ENABLE_DOCX_OUTPUT=0 -DFZ_ENABLE_ODT_OUTPUT=0"
export EMSDK_QUIET=1
source $EMSDK_DIR/emsdk_env.sh
echo
echo BUILDING MUPDF CORE
make -j4 -C $MUPDF_DIR build=release OS=wasm XCFLAGS="$MUPDF_OPTS" libs
echo
echo BUILDING MUPDF WASM
emcc -o lib/mupdf-wasm.js -I $MUPDF_DIR/include lib/mupdf.c \
--no-entry \
-sABORTING_MALLOC=0 \
-sALLOW_MEMORY_GROWTH=1 \
-sMODULARIZE=1 \
-sNODEJS_CATCH_EXIT=0 \
-sWASM_ASYNC_COMPILATION=0 \
-sEXPORT_NAME='"libmupdf"' \
-sEXPORTED_RUNTIME_METHODS='["ccall","UTF8ToString","lengthBytesUTF8","stringToUTF8"]' \
$MUPDF_DIR/build/wasm/release/libmupdf.a \
$MUPDF_DIR/build/wasm/release/libmupdf-third.a
echo
+814
View File
@@ -0,0 +1,814 @@
<!DOCTYPE html>
<html>
<head>
<title>Librera Online Book Reader (EPUB, PDF, FB2, CBZ)</title>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<style>
:root {
--menubar-height: 32pt;
--sidebar-width: 250px;
}
html, body { margin: 0; padding: 0; }
html { background-color: gray; }
input, button {
display: block;
border:none;
background-image:none;
background-color:transparent;
box-shadow: none;
}
input, button {
font-size: 16px;
line-height: 20px;
height: 24px;
margin: 0 4px;
}
input {
background-color: white;
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 4px;
transition: all 0.2s ease;
font-size: 14px;
color: #333;
}
input:focus {
outline: none;
border-color: #3f51b5;
box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.2);
background-color: #fafafa;
}
input:hover {
border-color: #999;
background-color: #f9f9f9;
}
button {
font-family: monospace;
background-color: gainsboro;
border: 2px outset white;
outline: 1px solid black;
padding: 1px 10px 1px 10px;
}
button:disabled {
color: gray;
border: 2px solid gainsboro;
outline: 1px solid gray;
}
button:enabled:active:hover {
border: 2px inset white;
padding: 2px 9px 0px 11px;
}
/* MENUS */
#grid-menubar { font-family: "Segoe UI", "Arial", "sans-serif"; }
#grid-menubar {
position: fixed;
z-index: 3;
top: 0;
left: 0;
font-size: 12pt;
line-height: 1;
height: var(--menubar-height);
width: 100%;
background-color: lightsteelblue;
user-select: none;
box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.1);
display: flex;
align-items: center;
padding-left: 32px;
}
.menu {
display: flex;
align-items: center;
}
.menu-button { padding: 5pt 8pt; }
.menu-popup {
display: none;
position: absolute;
background-color: white;
min-width: 20ex;
white-space: nowrap;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 3;
}
.menu-popup hr { margin: 0; border-bottom: 0; border-top: 1px solid gray; }
.menu-popup div { padding: 5pt 10pt; }
.menu-popup div:hover { background-color: steelblue; color: white; }
.menu:hover .menu-popup { display: block; }
.menu:hover .menu-button { background-color: steelblue; color: white; }
/* Enhanced hover effects for menu buttons */
.menu-button {
transition: all 0.2s ease;
cursor: pointer;
border-radius: 4px;
}
.menu-button:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.menu-button:active {
transform: scale(0.95);
}
/* SIDEBAR - Local Design */
#grid-sidebar {
display: none;
position: fixed;
z-index: 1;
top: var(--menubar-height);
left: 0;
width: var(--sidebar-width);
height: calc(100% - var(--menubar-height));
background-color: #f4f4f9;
border-right: 2px solid #ddd;
overflow-y: auto;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
border-bottom: 1px solid #ddd;
background-color: #f8f8f8;
}
.sidebar-title {
font-weight: bold;
font-size: 12pt;
color: #333;
}
.sidebar-close {
background: none;
border: none;
font-size: 16px;
color: #666;
cursor: pointer;
padding: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.sidebar-close:hover {
background-color: #e0e0e0;
color: #333;
}
#outline {
margin: 0;
padding: 10px 0;
list-style-type: none;
font-size: 15px;
}
#outline ul {
margin: 0;
padding: 0;
list-style-type: none;
}
#outline a {
display: block;
padding: 10px 15px;
text-decoration: none;
color: #333;
border-left: 3px solid transparent;
transition: background-color 0.3s, border-left 0.3s;
}
#outline a:hover {
background-color: #e0e0e0;
border-left: 3px solid #3f51b5;
color: #3f51b5;
}
#outline a.active {
background-color: #d1d1d1;
border-left: 3px solid #3f51b5;
color: #3f51b5;
}
.outline-item-expandable {
position: relative;
}
.outline-item-expandable::after {
content: '\25BA';
position: absolute;
right: 15px;
font-size: 12px;
color: #444;
transition: transform 0.3s;
}
.outline-item-expandable.expanded::after {
transform: rotate(90deg);
}
/* DIALOGS - Modern Local Design */
div.dialog {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 370px;
max-width: 90vw;
background-color: #e0f2f1;
border-radius: 8px;
padding: 16px;
z-index: 1000;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24), 0 4px 16px rgba(0, 0, 0, 0.12);
border: 1px solid #b2dfdb;
}
@media screen and (max-width: 480px) {
div.dialog {
top: 5px;
left: 5px;
right: 5px;
width: auto;
max-width: none;
transform: none;
padding: 12px;
}
}
div.dialog::backdrop,
div.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.dialog-close {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.dialog-close:hover {
background-color: #f0f0f0;
color: #333;
}
.dialog-content {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
font-family: inherit;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.search-input:focus {
border-color: #3f51b5;
box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1);
}
@media screen and (max-width: 480px) {
#search-text {
font-size: 14px;
padding: 8px 12px;
}
}
.search-options {
display: flex;
gap: 12px;
margin-top: 12px;
}
.search-checkbox {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #666;
}
.search-checkbox input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #3f51b5;
}
div.flex {
display: flex;
align-items: center;
gap: 12px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #b2dfdb;
background-color: transparent;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
min-width: 100px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: linear-gradient(135deg, #3f51b5 0%, #5c6bc0 100%);
color: #ffffff;
border: 2px solid transparent;
}
.btn-primary:hover {
background: linear-gradient(135deg, #3949ab 0%, #5e35b1 100%);
border: 2px solid #fff;
}
.btn-secondary {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
color: #3f51b5;
border: 2px solid #3f51b5;
}
.btn-secondary:hover {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
color: #3949ab;
border: 2px solid #3949ab;
}
#search-status {
padding: 12px 0;
color: #666;
font-size: 14px;
font-style: italic;
}
.search-results {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-top: 12px;
}
.search-result-item {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
}
.search-result-item:hover {
background-color: #f8f9fa;
}
.search-result-item:last-child {
border-bottom: none;
}
/* PAGES */
#grid-main { padding-top: var(--menubar-height); }
#grid-main.sidebarVisible { padding-left: var(--sidebar-width); }
#grid-main.sidebarHidden { padding-left: 0; }
#pages { margin: 0 auto; }
#placeholder { margin: 0 auto; }
#placeholder div {
padding-top:3em;
text-align: center;
font-size: 24pt;
font-weight: bold;
color: silver;
}
#placeholder div.error {
text-align: left;
color: hotpink;
}
div.error {
padding: 1em;
color: hotpink;
font-size: 20pt;
}
a.anchor {
display:block;
position:relative;
top:-22pt;
visibility:hidden;
}
div.page {
position:relative;
background-color:white;
margin:16px auto;
box-shadow: 0px 4px 16px 0px rgba(0,0,0,0.2);
}
div.page canvas {
position:absolute;
user-select:none;
}
div.links { position:absolute; }
div.links a { position:absolute; }
div.links a:hover { outline: 1px dotted blue; }
div.text { position:absolute; }
div.text span {
position:absolute;
white-space:pre;
line-height:1;
color:transparent;
}
div.text ::selection {
background: rgba(0, 10, 240, 0.4);
}
@media screen and (max-width: 1000px) {
.document-title {
display: none !important;
}
}
div.searchHitList { position:absolute; }
div.searchHit { position:absolute; pointer-events:none; outline: 1px solid hotpink; background-color: lightpink; mix-blend-mode: multiply; }
/* ICON HOVER EFFECTS */
#menu-toggle {
transition: all 0.2s ease;
}
#menu-toggle:hover {
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.1);
border-radius: 3px;
padding: 2px;
}
#menu-toggle:hover span {
background: #000 !important;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.icon-button {
transition: all 0.2s ease;
}
.icon-button:hover {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 6px;
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.icon-button:hover svg {
stroke: #000;
stroke-width: 2.5;
}
.icon-button:active {
transform: scale(0.95);
background-color: rgba(255, 255, 255, 0.3);
}
</style>
<script>
let fileName = ""
let isShow = 1
function hide (elements) {
elements = elements.length ? elements : [elements];
for (var index = 0; index < elements.length; index++) {
elements[index].style.display = 'none';
}
}
function show (elements) {
elements = elements.length ? elements : [elements];
for (var index = 0; index < elements.length; index++) {
// Preserve original display style for menubar
if (elements[index].id === 'grid-menubar') {
elements[index].style.display = 'flex';
} else {
elements[index].style.display = 'block';
}
}
}
function showHide(){
if(isShow){
hide(document.getElementById('grid-menubar'))
isShow = 0;
}else{
show(document.getElementById('grid-menubar'))
isShow = 1;
}
}
function closeSidebar(){
hide(document.getElementById('grid-sidebar'))
document.getElementById('grid-main').className = 'sidebarHidden';
}
</script>
</head>
<body>
<button id="menu-toggle" onclick="showHide()" style="
z-index: 10;
position: fixed;
top: 13px;
left: 8px;
width: 16px;
height: 14px;
background: none;
border: none;
outline: none;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 0;
margin: 0;
">
<span style="
display: block;
width: 16px;
height: 2px;
background: #333;
"></span>
<span style="
display: block;
width: 16px;
height: 2px;
background: #333;
"></span>
<span style="
display: block;
width: 16px;
height: 2px;
background: #333;
"></span>
</button>
<div id="grid-menubar">
<div class="menu">
<div class="menu-button" onclick="document.getElementById('open-file-input').click()">
Open...
</div>
</div>
<div class="menu">
<button class="icon-button" onclick="documentViewer.toggleOutline()" title="Toggle Outline" style="
background: none;
border: none;
outline: none;
cursor: pointer;
padding: 10px;
margin: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="8" y1="6" x2="21" y2="6"></line>
<line x1="8" y1="12" x2="21" y2="12"></line>
<line x1="8" y1="18" x2="21" y2="18"></line>
<line x1="3" y1="6" x2="3.01" y2="6"></line>
<line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
</button>
</div>
<div class="menu">
<button class="icon-button" onclick="documentViewer.showSearchBox()" title="Search" style="
background: none;
border: none;
outline: none;
cursor: pointer;
padding: 10px;
margin: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
</div>
<div class="menu">
<input min="1" max="100" onchange="if(fileName) documentViewer.openFile(fileName);"
type="number" id="document-font-size" size="2" value="14"
placeholder="Font size" title="Font Size"/>
</div>
<div class="menu">
<input onchange="if(fileName) documentViewer.openFile(fileName);" type="text"
id="document-width" size="3" value="500" placeholder="Width" title="Document Width"/>
</div>
<div class="menu">
<input onchange="if(fileName) documentViewer.openFile(fileName);" type="text"
id="document-height" size="3" value="650" placeholder="Height" title="Document Height"/>
</div>
</div>
<div id="grid-sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Outline</span>
<button class="sidebar-close" onclick="closeSidebar()" title="Close outline">×</button>
</div>
<ul id="outline"></ul>
</div>
<div id="grid-main" class="sidebarHidden">
<div id="pages">
</div>
<div id="placeholder">
<div>
Loading, please wait...
</div>
</div>
</div>
<div id="search-dialog" class="dialog" style="display:none">
</div>
<input type="file" id="open-file-input" style="display:none"
accept=".fb2,.epub,.cbz,.pdf,.mobi,.txt,application/pdf"
oninput="fileName=event.target.files[0]; documentViewer.openFile(fileName);"/>
</body>
<script src="mupdf-view.js"></script>
<script src="mupdf-view-page.js"></script>
<script>
"use strict";
var height = window.innerHeight;
var width = window.innerWidth;
//alert(height+"x"+width);
if(width >height){
width = height*0.75*0.75
}else {
width = width*0.75
}
document.getElementById("document-width").value=Math.round(width);
document.getElementById("document-height").value=Math.round(height*0.70);
let params = new URLSearchParams(window.location.search);
let documentViewer = new MupdfDocumentViewer(mupdfView)
mupdfView.ready
.then(function () {
if (params.has("file")) {
documentViewer.openURL(params.get("file"), params.get("progressive") | 0, params.get("prefetch") | 0);
}
else {
documentViewer.openEmpty();
}
})
.catch(function (error) {
documentViewer.showDocumentError("Load", error);
});
window.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
documentViewer.hideSearchBox();
}
if (event.ctrlKey || event.metaKey) {
switch (event.keyCode) {
// '=' / '+' on various keyboards
case 61:
case 107:
case 187:
case 171:
documentViewer.zoomIn();
event.preventDefault();
break;
// '-'
case 173:
case 109:
case 189:
documentViewer.zoomOut();
event.preventDefault();
break;
// '0'
case 48:
case 96:
documentViewer.setZoom(100);
break;
case 70: // 'F':
event.preventDefault();
documentViewer.showSearchBox();
break;
case 71: // 'G':
event.preventDefault();
documentViewer.showSearchBox();
runSearch(event.shiftKey ? -1 : 1);
break;
}
}
});
</script>
</html>
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+968
View File
@@ -0,0 +1,968 @@
// 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
}
}
+233
View File
@@ -0,0 +1,233 @@
// 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.
/* global mupdf */
"use strict"
// Import the WASM module.
globalThis.__filename = "lib/mupdf-wasm.js"
importScripts("lib/mupdf-wasm.js")
// Import the MuPDF bindings.
importScripts("lib/mupdf.js")
const workerMethods = {}
onmessage = async function (event) {
let [ func, id, args ] = event.data
try {
let result = workerMethods[func](...args)
postMessage([ "RESULT", id, result ])
} catch (error) {
if (error instanceof mupdf.TryLaterError) {
trylaterQueue.push(event)
} else {
postMessage([ "ERROR", id, { name: error.name, message: error.message, stack: error.stack } ])
}
}
}
let trylaterScheduled = false
let trylaterQueue = []
mupdf.onFetchCompleted = function (_id) {
if (!trylaterScheduled) {
trylaterScheduled = true
setTimeout(() => {
trylaterScheduled = false
let currentQueue = trylaterQueue
trylaterQueue = []
currentQueue.forEach(onmessage)
}, 0)
}
}
let openStream = null
let openDocument = null
workerMethods.setLogFilters = function (filters) {
logFilters = filters
}
workerMethods.openStreamFromUrl = function (url, contentLength, progressive, prefetch) {
openStream = new mupdf.Stream(url, contentLength, Math.max(progressive << 10, 1 << 16), prefetch)
}
workerMethods.openDocumentFromBuffer = function (buffer, magic) {
openDocument = mupdf.Document.openDocument(buffer, magic)
}
workerMethods.openDocumentFromStream = function (magic) {
if (openStream == null) {
throw new Error("openDocumentFromStream called but no stream has been open")
}
openDocument = mupdf.Document.openDocument(openStream, magic)
}
workerMethods.freeDocument = function () {
openDocument?.destroy()
openDocument = null
}
workerMethods.documentTitle = function () {
return openDocument.getMetaData(Document.META_INFO_TITLE)
}
workerMethods.documentOutline = function () {
return openDocument.loadOutline()
}
workerMethods.countPages = function () {
return openDocument.countPages()
}
workerMethods.layout = function (w, h, em) {
return openDocument.layout(w, h, em)
}
workerMethods.userCSS = function (css) {
return openDocument.userCSS(css)
}
// TODO - use hungarian notation for coord spaces
// TODO - document the "- 1" better
// TODO - keep page loaded?
workerMethods.getPageSize = function (pageNumber) {
let page = openDocument.loadPage(pageNumber - 1)
let bounds = page.getBounds()
return { width: bounds[2] - bounds[0], height: bounds[3] - bounds[1] }
}
workerMethods.getPageLinks = function (pageNumber) {
let page = openDocument.loadPage(pageNumber - 1)
let links = page.getLinks()
return links.map((link) => {
const [ x0, y0, x1, y1 ] = link.getBounds()
let href
if (link.isExternal()) {
href = link.getURI()
} else {
const linkPageNumber = openDocument.resolveLink(link)
// TODO - move to front-end
// TODO - document the "+ 1" better
href = `#page${linkPageNumber + 1}`
}
return {
x: x0,
y: y0,
w: x1 - x0,
h: y1 - y0,
href,
}
})
}
workerMethods.getPageText = function (pageNumber) {
let page = openDocument.loadPage(pageNumber - 1)
let text = page.toStructuredText(1).asJSON()
return JSON.parse(text)
}
workerMethods.search = function (pageNumber, needle) {
let page = openDocument.loadPage(pageNumber - 1)
const hits = page.search(needle)
let result = []
for (let hit of hits) {
for (let quad of hit) {
const [ ulx, uly, urx, ury, llx, lly, lrx, lry ] = quad
result.push({
x: ulx,
y: uly,
w: urx - ulx,
h: lly - uly,
})
}
}
return result
}
workerMethods.getPageAnnotations = function (pageNumber, dpi) {
let page = openDocument.loadPage(pageNumber - 1)
if (page == null) {
return []
}
const annotations = page.getAnnotations()
const doc_to_screen = [ dpi = 72, 0, 0, dpi / 72, 0, 0 ]
return annotations.map((annotation) => {
const [ x0, y0, x1, y1 ] = Matrix.transformRect(annotation.getBounds())
return {
x: x0,
y: y0,
w: x1 - x0,
h: y1 - y0,
type: annotation.getType(),
ref: annotation.pointer,
}
})
}
// TODO - Move this to mupdf-view
const lastPageRender = new Map()
workerMethods.drawPageAsPNG = function (pageNumber, dpi) {
const doc_to_screen = mupdf.Matrix.scale(dpi / 72, dpi / 72)
// TODO - use canvas?
let page = openDocument.loadPage(pageNumber - 1)
let pixmap = page.toPixmap(doc_to_screen, mupdf.DeviceRGB, false)
let png = pixmap?.saveAsPNG()
pixmap?.destroy()
return png
}
workerMethods.drawPageAsPixmap = function (pageNumber, dpi) {
const doc_to_screen = mupdf.Matrix.scale(dpi / 72, dpi / 72)
let page = openDocument.loadPage(pageNumber - 1)
let bbox = Rect.transform(page.getBounds(), doc_to_screen)
let pixmap = new mupdf.Pixmap(mupdf.ColorSpace.DeviceRGB, bbox, true)
pixmap.clear(255)
let device = new mupdf.DrawDevice(doc_to_screen, pixmap)
page.run(device, Matrix.identity)
device.close()
let pixArray = pixmap.getPixels()
let pixW = pixmap.getWidth()
let pixH = pixmap.getHeight()
let imageData = new ImageData(pixArray.slice(), pixW, pixH)
pixmap.destroy()
return imageData
}
postMessage([ "READY", Object.keys(workerMethods) ])
+92
View File
@@ -0,0 +1,92 @@
// 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.
"use strict"
var mupdfView = {}
const worker = new Worker("mupdf-view-worker.js")
const messagePromises = new Map()
let lastPromiseId = 0
mupdfView.ready = new Promise((resolve, reject) => {
worker.onmessage = function (event) {
let type = event.data[0]
if (type === "READY") {
let methodNames = event.data[1]
for (let method of methodNames)
mupdfView[method] = wrap(method)
worker.onmessage = onWorkerMessage
resolve()
} else if (type === "ERROR") {
let error = event.data[1]
reject(new Error(error))
} else {
reject(new Error(`Unexpected first message: ${event.data}`))
}
}
})
function onWorkerMessage(event) {
let [ type, id, result ] = event.data
if (type === "RESULT")
messagePromises.get(id).resolve(result)
else if (type === "READY")
messagePromises.get(id).reject(new Error("Unexpected READY message"))
else if (type === "ERROR") {
let error = new Error(result.message)
error.name = result.name
error.stack = result.stack
messagePromises.get(id).reject(error)
} else
messagePromises.get(id).reject(new Error(`Unexpected result type '${type}'`))
messagePromises.delete(id)
}
// TODO - Add cancelation for trylater queues
function wrap(func) {
return function (...args) {
return new Promise(function (resolve, reject) {
let id = lastPromiseId++
messagePromises.set(id, { resolve, reject })
if (args[0] instanceof ArrayBuffer)
worker.postMessage([ func, id, args ], [ args[0] ])
else
worker.postMessage([ func, id, args ])
})
}
}
mupdfView.setLogFilters = wrap("setLogFilters")
const wrap_openStreamFromUrl = wrap("openStreamFromUrl")
const wrap_openDocumentFromStream = wrap("openDocumentFromStream")
mupdfView.openDocumentFromUrl = async function (url, contentLength, progressive, prefetch, magic) {
await wrap_openStreamFromUrl(url, contentLength, progressive, prefetch)
return await wrap_openDocumentFromStream(magic)
}
mupdfView.terminate = function () {
worker.terminate()
}