Gast commit!
This commit is contained in:
@@ -0,0 +1 @@
|
||||
mupdf-
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) ])
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user