mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
04e5cd7071
commit
fb4ba04767
5 files changed
+162
-159
No files matched your search
@@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
// FS WebUI嵌入静态文件
|
// FS WebUI嵌入静态文件
|
||||||
//
|
//
|
||||||
//go:embed index.html ofdgo.css ofdgo.js ofdgo.json ofdgo.svg ofdgo.sw.js ofdgo.wasm wasm_exec.js
|
//go:embed index.html ofdgo.css ofdgo.js ofdgo.json ofdgo.svg ofdgo.sw.js ofdgo.wasm ofdgo.wasm.js wasm_exec.js
|
||||||
var FS embed.FS
|
var FS embed.FS
|
||||||
|
|
||||||
// Checksum 计算WebUI嵌入资源校验值
|
// Checksum 计算WebUI嵌入资源校验值
|
||||||
|
|||||||
@@ -152,7 +152,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./wasm_exec.js"></script>
|
|
||||||
<script type="module" src="./ofdgo.js"></script>
|
<script type="module" src="./ofdgo.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+125
-154
@@ -13,19 +13,11 @@ const STATUS = {
|
|||||||
exporting: "正在导出文档",
|
exporting: "正在导出文档",
|
||||||
pageExporting: "正在导出单页",
|
pageExporting: "正在导出单页",
|
||||||
};
|
};
|
||||||
const WASM_CALLBACKS = [
|
const wasmRequests = new Map();
|
||||||
"ofdgoOpen",
|
|
||||||
"ofdgoConfigure",
|
|
||||||
"ofdgoDocumentInfo",
|
|
||||||
"ofdgoRenderPage",
|
|
||||||
"ofdgoExportFormats",
|
|
||||||
"ofdgoExportPage",
|
|
||||||
"ofdgoExportPDF",
|
|
||||||
"ofdgoFontFileMatches",
|
|
||||||
];
|
|
||||||
|
|
||||||
let wasmPromise = null;
|
let wasmPromise = null;
|
||||||
let wasmModule = null;
|
let wasmWorker = null;
|
||||||
|
let wasmRequestID = 0;
|
||||||
let wasmRecoveryTimer = 0;
|
let wasmRecoveryTimer = 0;
|
||||||
let fontDatabase = null;
|
let fontDatabase = null;
|
||||||
|
|
||||||
@@ -35,9 +27,11 @@ const state = {
|
|||||||
wasmSeq: 0,
|
wasmSeq: 0,
|
||||||
wasmRecovering: false,
|
wasmRecovering: false,
|
||||||
wasmRecoveries: 0,
|
wasmRecoveries: 0,
|
||||||
|
exporting: false,
|
||||||
ofdBytes: null,
|
ofdBytes: null,
|
||||||
fileName: "ofdgo.ofd",
|
fileName: "ofdgo.ofd",
|
||||||
openSeq: 0,
|
openSeq: 0,
|
||||||
|
pageSeq: 0,
|
||||||
fontSeq: 0,
|
fontSeq: 0,
|
||||||
localFonts: [],
|
localFonts: [],
|
||||||
userFonts: [],
|
userFonts: [],
|
||||||
@@ -275,7 +269,7 @@ async function boot() {
|
|||||||
fontsRestored = false;
|
fontsRestored = false;
|
||||||
}
|
}
|
||||||
setProgress("引擎准备完成", 70);
|
setProgress("引擎准备完成", 70);
|
||||||
loadExportFormats();
|
await loadExportFormats();
|
||||||
setEmpty("选择 OFD 文件");
|
setEmpty("选择 OFD 文件");
|
||||||
updateFontSummary();
|
updateFontSummary();
|
||||||
renderFontList();
|
renderFontList();
|
||||||
@@ -419,8 +413,8 @@ async function refreshApplication() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExportFormats() {
|
async function loadExportFormats() {
|
||||||
const formats = callWASM("ofdgoExportFormats") || [];
|
const formats = await callWASM("ofdgoExportFormats") || [];
|
||||||
state.exportFormats = formats;
|
state.exportFormats = formats;
|
||||||
el.pageExportFormat.replaceChildren();
|
el.pageExportFormat.replaceChildren();
|
||||||
for (const format of formats) {
|
for (const format of formats) {
|
||||||
@@ -448,47 +442,41 @@ async function ensureWASM() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadWASM() {
|
async function loadWASM() {
|
||||||
if (!globalThis.Go) {
|
|
||||||
throw new Error("渲染引擎脚本缺失");
|
|
||||||
}
|
|
||||||
const wasmSeq = state.wasmSeq + 1;
|
const wasmSeq = state.wasmSeq + 1;
|
||||||
state.wasmSeq = wasmSeq;
|
state.wasmSeq = wasmSeq;
|
||||||
state.ready = false;
|
state.ready = false;
|
||||||
state.wasmExited = false;
|
state.wasmExited = false;
|
||||||
clearWASMCallbacks();
|
await new Promise((resolve, reject) => {
|
||||||
const go = new Go();
|
const worker = new Worker("./ofdgo.wasm.js");
|
||||||
if (!wasmModule) {
|
wasmWorker = worker;
|
||||||
setProgress("正在下载引擎", 16);
|
const fail = (err) => {
|
||||||
const response = await fetch("./ofdgo.wasm");
|
markWASMExited(wasmSeq, err);
|
||||||
try {
|
reject(err);
|
||||||
setProgress("正在编译引擎", 35);
|
};
|
||||||
wasmModule = await WebAssembly.compileStreaming(response);
|
worker.onerror = (event) => fail(new Error(event.message || "渲染引擎加载失败"));
|
||||||
} catch {
|
worker.onmessageerror = () => fail(new Error("渲染引擎通信失败"));
|
||||||
const fallback = await fetch("./ofdgo.wasm");
|
worker.onmessage = ({ data }) => {
|
||||||
const bytes = await fallback.arrayBuffer();
|
if (worker !== wasmWorker) {
|
||||||
setProgress("正在编译引擎", 45);
|
return;
|
||||||
wasmModule = await WebAssembly.compile(bytes);
|
}
|
||||||
}
|
if (data.type === "progress") {
|
||||||
}
|
setProgress(data.text, data.percent);
|
||||||
setProgress("正在启动引擎", 58);
|
} else if (data.type === "ready") {
|
||||||
const instance = await WebAssembly.instantiate(wasmModule, go.importObject);
|
state.ready = true;
|
||||||
go.run(instance).then(() => {
|
resolve();
|
||||||
markWASMExited(wasmSeq);
|
} else if (data.type === "exit") {
|
||||||
}).catch((err) => {
|
fail(new Error(data.error));
|
||||||
markWASMExited(wasmSeq, err);
|
} else {
|
||||||
|
const request = wasmRequests.get(data.id);
|
||||||
|
wasmRequests.delete(data.id);
|
||||||
|
if (data.ok) {
|
||||||
|
request.resolve(data.data);
|
||||||
|
} else {
|
||||||
|
request.reject(new Error(data.error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
await waitFor(() => WASM_CALLBACKS.every((name) => typeof globalThis[name] === "function"));
|
|
||||||
if (wasmSeq !== state.wasmSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.ready = true;
|
|
||||||
state.wasmExited = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWASMCallbacks() {
|
|
||||||
for (const name of WASM_CALLBACKS) {
|
|
||||||
globalThis[name] = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
||||||
@@ -497,6 +485,12 @@ function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
|||||||
}
|
}
|
||||||
state.ready = false;
|
state.ready = false;
|
||||||
state.wasmExited = true;
|
state.wasmExited = true;
|
||||||
|
wasmWorker?.terminate();
|
||||||
|
wasmWorker = null;
|
||||||
|
for (const request of wasmRequests.values()) {
|
||||||
|
request.reject(err || new Error("渲染引擎已退出"));
|
||||||
|
}
|
||||||
|
wasmRequests.clear();
|
||||||
if (err) {
|
if (err) {
|
||||||
setStatus("渲染引擎异常,正在恢复");
|
setStatus("渲染引擎异常,正在恢复");
|
||||||
}
|
}
|
||||||
@@ -517,43 +511,28 @@ async function recoverWASM() {
|
|||||||
}
|
}
|
||||||
state.wasmRecovering = true;
|
state.wasmRecovering = true;
|
||||||
state.wasmRecoveries += 1;
|
state.wasmRecoveries += 1;
|
||||||
const pageIndex = state.pageIndex;
|
const openSeq = ++state.openSeq;
|
||||||
const fitMode = state.fitMode || "width";
|
|
||||||
setBusy(true, "正在恢复引擎", 18, STATUS.recovering);
|
setBusy(true, "正在恢复引擎", 18, STATUS.recovering);
|
||||||
try {
|
try {
|
||||||
await ensureWASM();
|
|
||||||
await openDocument({
|
await openDocument({
|
||||||
pageIndex,
|
pageIndex: state.pageIndex,
|
||||||
fitMode,
|
fitMode: state.fitMode || "width",
|
||||||
|
scale: state.scale,
|
||||||
skipAutoFonts: true,
|
skipAutoFonts: true,
|
||||||
|
openSeq,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err, !state.doc);
|
if (openSeq === state.openSeq) {
|
||||||
|
showError(err, !state.doc);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.wasmRecovering = false;
|
state.wasmRecovering = false;
|
||||||
if (!state.doc) {
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitFor(predicate) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const started = performance.now();
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
if (predicate()) {
|
|
||||||
window.clearInterval(timer);
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (performance.now() - started > 5000) {
|
|
||||||
window.clearInterval(timer);
|
|
||||||
reject(new Error("渲染引擎初始化超时"));
|
|
||||||
}
|
|
||||||
}, 20);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openOFD(file) {
|
async function openOFD(file) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
@@ -564,14 +543,21 @@ async function openOFD(file) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.wasmRecoveries = 0;
|
state.wasmRecoveries = 0;
|
||||||
|
const openSeq = ++state.openSeq;
|
||||||
setBusy(true, "正在读取 OFD", 10, STATUS.opening);
|
setBusy(true, "正在读取 OFD", 10, STATUS.opening);
|
||||||
try {
|
try {
|
||||||
state.ofdBytes = new Uint8Array(await file.arrayBuffer());
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.ofdBytes = bytes;
|
||||||
state.fileName = file.name || "ofdgo.ofd";
|
state.fileName = file.name || "ofdgo.ofd";
|
||||||
await openDocument({ pageIndex: 0, resetScroll: true });
|
await openDocument({ pageIndex: 0, resetScroll: true, openSeq });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err, true);
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
showError(err, true);
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,9 +804,12 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3));
|
const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3));
|
||||||
let selected = selectLocalFonts(available, docNames, docLoadLimit);
|
let selected = await selectLocalFonts(available, docNames, docLoadLimit);
|
||||||
if (!selected.length) {
|
if (!selected.length) {
|
||||||
selected = selectLocalFonts(available, [], 6);
|
selected = await selectLocalFonts(available, [], 6);
|
||||||
|
}
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到所需字体";
|
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到所需字体";
|
||||||
const fonts = [];
|
const fonts = [];
|
||||||
@@ -860,7 +849,7 @@ function uniqueLocalFonts(fonts) {
|
|||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectLocalFonts(fonts, names, limit) {
|
async function selectLocalFonts(fonts, names, limit) {
|
||||||
const available = new Map();
|
const available = new Map();
|
||||||
for (const font of uniqueLocalFonts(fonts)) {
|
for (const font of uniqueLocalFonts(fonts)) {
|
||||||
available.set(localFontName(font), font);
|
available.set(localFontName(font), font);
|
||||||
@@ -870,7 +859,7 @@ function selectLocalFonts(fonts, names, limit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const matched = callWASM("ofdgoFontFileMatches", [...available.keys()], names);
|
const matched = await callWASM("ofdgoFontFileMatches", [...available.keys()], names);
|
||||||
return uniqueLocalFonts(matched.map((name) => available.get(name))).slice(0, limit);
|
return uniqueLocalFonts(matched.map((name) => available.get(name))).slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,6 +985,9 @@ async function applyFontChange(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAnnotations() {
|
async function toggleAnnotations() {
|
||||||
|
if (document.body.hasAttribute("aria-busy")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.renderAnnotations = !state.renderAnnotations;
|
state.renderAnnotations = !state.renderAnnotations;
|
||||||
updateAnnotationButton();
|
updateAnnotationButton();
|
||||||
if (!state.ofdBytes || !state.doc) {
|
if (!state.ofdBytes || !state.doc) {
|
||||||
@@ -1161,10 +1153,13 @@ async function openDocument(options = {}) {
|
|||||||
if (!state.ofdBytes) {
|
if (!state.ofdBytes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const openSeq = options.openSeq || (state.openSeq += 1);
|
||||||
if (!state.ready || state.wasmExited) {
|
if (!state.ready || state.wasmExited) {
|
||||||
await ensureWASM();
|
await ensureWASM();
|
||||||
}
|
}
|
||||||
const openSeq = options.openSeq || (state.openSeq += 1);
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resetLocalFonts = !options.skipAutoFonts;
|
const resetLocalFonts = !options.skipAutoFonts;
|
||||||
if (resetLocalFonts) {
|
if (resetLocalFonts) {
|
||||||
state.localFonts = [];
|
state.localFonts = [];
|
||||||
@@ -1184,8 +1179,8 @@ async function openDocument(options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = options.reuseSession
|
const doc = options.reuseSession
|
||||||
? callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
|
? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
|
||||||
: callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations);
|
: await callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1251,7 +1246,7 @@ async function loadDocumentDetails(openSeq) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const info = callWASM("ofdgoDocumentInfo");
|
const info = await callWASM("ofdgoDocumentInfo");
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
Object.assign(state.doc, info, { detailsPending: false });
|
Object.assign(state.doc, info, { detailsPending: false });
|
||||||
renderMeta();
|
renderMeta();
|
||||||
@@ -1275,10 +1270,13 @@ async function renderPage(index, options = {}) {
|
|||||||
if (index < 0 || index >= pageCount) {
|
if (index < 0 || index >= pageCount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.keepBusy) {
|
const pageSeq = ++state.pageSeq;
|
||||||
setProgress("正在渲染页面", 76);
|
if (!state.exporting) {
|
||||||
} else {
|
if (options.keepBusy) {
|
||||||
setBusy(true, "正在渲染页面", 35, "正在渲染页面");
|
setProgress("正在渲染页面", 76);
|
||||||
|
} else {
|
||||||
|
setBusy(true, "正在渲染页面", 35, "正在渲染页面");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setCurrentPage(index);
|
setCurrentPage(index);
|
||||||
@@ -1289,7 +1287,7 @@ async function renderPage(index, options = {}) {
|
|||||||
scrollToPage(index);
|
scrollToPage(index);
|
||||||
}
|
}
|
||||||
await renderFlowPage(index, { throwError: true, openSeq, priority: 0 });
|
await renderFlowPage(index, { throwError: true, openSeq, priority: 0 });
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq || index !== state.pageIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.scroll !== false) {
|
if (options.scroll !== false) {
|
||||||
@@ -1301,30 +1299,22 @@ async function renderPage(index, options = {}) {
|
|||||||
showError(err, false);
|
showError(err, false);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!options.keepBusy && openSeq === state.openSeq) {
|
if (!options.keepBusy && !state.exporting && openSeq === state.openSeq && pageSeq === state.pageSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportPDF() {
|
async function exportPDF() {
|
||||||
if (!state.doc) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setExportControlsDisabled(true);
|
state.exporting = true;
|
||||||
setBusy(true, "正在准备 PDF", 18, STATUS.exporting);
|
updateControls();
|
||||||
|
setBusy(true, "正在生成 PDF", 45, STATUS.exporting);
|
||||||
try {
|
try {
|
||||||
await waitForPaint();
|
const result = await callWASM("ofdgoExportPDF");
|
||||||
if (openSeq !== state.openSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setProgress("正在生成 PDF", 45);
|
|
||||||
await waitForPaint();
|
|
||||||
if (openSeq !== state.openSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = callWASM("ofdgoExportPDF");
|
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1337,6 +1327,7 @@ async function exportPDF() {
|
|||||||
showError(err, false);
|
showError(err, false);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
state.exporting = false;
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -1345,7 +1336,7 @@ async function exportPDF() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exportCurrentPage() {
|
async function exportCurrentPage() {
|
||||||
if (!state.doc) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const format = el.pageExportFormat.value;
|
const format = el.pageExportFormat.value;
|
||||||
@@ -1355,32 +1346,26 @@ async function exportCurrentPage() {
|
|||||||
const info = exportFormatInfo(format);
|
const info = exportFormatInfo(format);
|
||||||
const label = info?.label || String(format || "").toUpperCase();
|
const label = info?.label || String(format || "").toUpperCase();
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setExportControlsDisabled(true);
|
const fileName = pageFileName(info?.extension || format);
|
||||||
setBusy(true, `正在准备 ${label}`, 18, STATUS.pageExporting);
|
state.exporting = true;
|
||||||
|
updateControls();
|
||||||
|
setBusy(true, `正在生成 ${label}`, 45, STATUS.pageExporting);
|
||||||
try {
|
try {
|
||||||
await waitForPaint();
|
|
||||||
if (openSeq !== state.openSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setProgress(`正在生成 ${label}`, 45);
|
|
||||||
await waitForPaint();
|
|
||||||
if (openSeq !== state.openSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dpi = exportFormatUsesDPI(format) ? currentImageDPI() : 0;
|
const dpi = exportFormatUsesDPI(format) ? currentImageDPI() : 0;
|
||||||
const result = callWASM("ofdgoExportPage", state.pageIndex, format, dpi);
|
const result = await callWASM("ofdgoExportPage", state.pageIndex, format, dpi);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProgress(`正在保存 ${result.label || label}`, 86);
|
setProgress(`正在保存 ${result.label || label}`, 86);
|
||||||
const bytes = result.bytes;
|
const bytes = result.bytes;
|
||||||
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", pageFileName(result.extension || info?.extension || format));
|
downloadBytes(bytes, result.mime || info?.mime || "application/octet-stream", fileName);
|
||||||
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
|
setStatus(`${result.label || label} 已导出 ${formatBytes(result.size || bytes.length, result.label || label)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
showError(err, false);
|
showError(err, false);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
state.exporting = false;
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -1571,7 +1556,7 @@ async function processPageRenderQueue() {
|
|||||||
delayed = true;
|
delayed = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const page = callWASM("ofdgoRenderPage", task.index);
|
const page = await callWASM("ofdgoRenderPage", task.index);
|
||||||
if (task.openSeq === state.openSeq) {
|
if (task.openSeq === state.openSeq) {
|
||||||
state.pageCache.set(task.index, page);
|
state.pageCache.set(task.index, page);
|
||||||
cacheThumbnail(task.index, page.svg);
|
cacheThumbnail(task.index, page.svg);
|
||||||
@@ -1714,7 +1699,7 @@ function setCurrentPage(index) {
|
|||||||
}
|
}
|
||||||
updatePageListCurrent();
|
updatePageListCurrent();
|
||||||
updateControls();
|
updateControls();
|
||||||
if (state.doc) {
|
if (state.doc && !state.exporting) {
|
||||||
setStatus(pageStatus(index, state.doc.pageCount));
|
setStatus(pageStatus(index, state.doc.pageCount));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2461,7 +2446,7 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
|||||||
restoreScaleAnchor(anchor);
|
restoreScaleAnchor(anchor);
|
||||||
}
|
}
|
||||||
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
||||||
if (updateStatus && state.doc) {
|
if (updateStatus && state.doc && !state.exporting) {
|
||||||
setStatus(`第 ${state.pageIndex + 1} / ${state.doc.pageCount} 页`);
|
setStatus(`第 ${state.pageIndex + 1} / ${state.doc.pageCount} 页`);
|
||||||
}
|
}
|
||||||
updateControls();
|
updateControls();
|
||||||
@@ -2559,9 +2544,9 @@ function updateControls() {
|
|||||||
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
|
||||||
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
|
||||||
updateAnnotationButton();
|
updateAnnotationButton();
|
||||||
el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length;
|
el.pageExportFormat.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||||
el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length;
|
el.exportPageButton.disabled = !hasDoc || state.exporting || !state.exportFormats.length;
|
||||||
el.exportButton.disabled = !hasDoc;
|
el.exportButton.disabled = !hasDoc || state.exporting;
|
||||||
updateDPIControl();
|
updateDPIControl();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2571,38 +2556,24 @@ function updateAnnotationButton() {
|
|||||||
el.annotationButton.setAttribute("aria-label", el.annotationButton.title);
|
el.annotationButton.setAttribute("aria-label", el.annotationButton.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setExportControlsDisabled(disabled) {
|
async function callWASM(name, ...args) {
|
||||||
el.exportButton.disabled = disabled;
|
|
||||||
el.exportPageButton.disabled = disabled;
|
|
||||||
el.pageExportFormat.disabled = disabled;
|
|
||||||
updateDPIControl(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callWASM(name, ...args) {
|
|
||||||
const fn = globalThis[name];
|
|
||||||
if (typeof fn !== "function") {
|
|
||||||
throw new Error("渲染引擎未初始化");
|
|
||||||
}
|
|
||||||
if (state.wasmExited) {
|
if (state.wasmExited) {
|
||||||
scheduleWASMRecovery();
|
scheduleWASMRecovery();
|
||||||
throw new Error(state.wasmRecovering ? "渲染引擎正在恢复" : "渲染引擎已退出,正在恢复");
|
throw new Error(state.wasmRecovering ? "渲染引擎正在恢复" : "渲染引擎已退出,正在恢复");
|
||||||
}
|
}
|
||||||
let payload;
|
if (!state.ready) {
|
||||||
try {
|
throw new Error("渲染引擎未初始化");
|
||||||
payload = fn(...args);
|
}
|
||||||
} catch (err) {
|
return new Promise((resolve, reject) => {
|
||||||
const message = String(err.message || err);
|
const id = ++wasmRequestID;
|
||||||
if (message.includes("Go program has already exited")) {
|
wasmRequests.set(id, { resolve, reject });
|
||||||
markWASMExited(state.wasmSeq);
|
try {
|
||||||
throw new Error("渲染引擎已退出,正在恢复");
|
wasmWorker.postMessage({ id, name, args });
|
||||||
|
} catch (err) {
|
||||||
|
wasmRequests.delete(id);
|
||||||
|
reject(err);
|
||||||
}
|
}
|
||||||
throw err;
|
});
|
||||||
}
|
|
||||||
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
||||||
if (!result.ok) {
|
|
||||||
throw new Error(result.error || "WASM 调用失败");
|
|
||||||
}
|
|
||||||
return result.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(err, empty = !state.doc) {
|
function showError(err, empty = !state.doc) {
|
||||||
@@ -2694,8 +2665,8 @@ function exportFormatUsesDPI(value) {
|
|||||||
return value === "png" || value === "jpg";
|
return value === "png" || value === "jpg";
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDPIControl(disabled = false) {
|
function updateDPIControl() {
|
||||||
el.imageDPI.disabled = disabled || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value);
|
el.imageDPI.disabled = state.exporting || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentImageDPI() {
|
function currentImageDPI() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
const CACHE_PREFIX = `ofdgo:${self.registration.scope}:app:`;
|
||||||
const META_CACHE = `${CACHE_PREFIX}meta`;
|
const META_CACHE = `${CACHE_PREFIX}meta`;
|
||||||
const ASSETS = ["./", "ofdgo.css", "ofdgo.js", "ofdgo.json", "ofdgo.svg", "ofdgo.sw.js", "ofdgo.wasm", "wasm_exec.js"]
|
const ASSETS = ["./", "ofdgo.css", "ofdgo.js", "ofdgo.json", "ofdgo.svg", "ofdgo.sw.js", "ofdgo.wasm", "ofdgo.wasm.js", "wasm_exec.js"]
|
||||||
.map((path) => new URL(path, self.registration.scope).href);
|
.map((path) => new URL(path, self.registration.scope).href);
|
||||||
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
const CLIENT_PREFIX = new URL("__client/", self.registration.scope).href;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ self.addEventListener("fetch", (event) => {
|
|||||||
if (event.request.method !== "GET" || !url.startsWith(self.registration.scope) || (navigation && url !== self.registration.scope)) {
|
if (event.request.method !== "GET" || !url.startsWith(self.registration.scope) || (navigation && url !== self.registration.scope)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.respondWith(navigator.locks.request(CACHE_PREFIX, { mode: navigation ? "exclusive" : "shared" }, () => readResource(event, navigation)));
|
event.respondWith(navigator.locks.request(CACHE_PREFIX, { mode: event.resultingClientId ? "exclusive" : "shared" }, () => readResource(event, navigation)));
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
@@ -85,13 +85,15 @@ async function readResource(event, navigation) {
|
|||||||
let bundle;
|
let bundle;
|
||||||
if (navigation) {
|
if (navigation) {
|
||||||
bundle = await currentBundle(meta);
|
bundle = await currentBundle(meta);
|
||||||
await meta.put(CLIENT_PREFIX + event.resultingClientId, Response.json(bundle));
|
|
||||||
} else {
|
} else {
|
||||||
bundle = await readBundle(meta, CLIENT_PREFIX + event.clientId);
|
bundle = await readBundle(meta, CLIENT_PREFIX + event.clientId);
|
||||||
}
|
}
|
||||||
if (!bundle || !await caches.has(bundle.name)) {
|
if (!bundle || !await caches.has(bundle.name)) {
|
||||||
return Response.error();
|
return Response.error();
|
||||||
}
|
}
|
||||||
|
if (event.resultingClientId) {
|
||||||
|
await meta.put(CLIENT_PREFIX + event.resultingClientId, Response.json(bundle));
|
||||||
|
}
|
||||||
const url = resourceURL(event.request.url);
|
const url = resourceURL(event.request.url);
|
||||||
if (!bundle.assets.includes(url)) {
|
if (!bundle.assets.includes(url)) {
|
||||||
return fetch(event.request);
|
return fetch(event.request);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
importScripts("./wasm_exec.js");
|
||||||
|
|
||||||
|
self.onmessage = ({ data: { id, name, args } }) => {
|
||||||
|
try {
|
||||||
|
const payload = globalThis[name](...args);
|
||||||
|
const result = typeof payload === "string" ? JSON.parse(payload) : payload;
|
||||||
|
self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []);
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ id, ok: false, error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function exitWASM(err) {
|
||||||
|
self.postMessage({ type: "exit", error: err?.message || "渲染引擎已退出" });
|
||||||
|
self.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWASM() {
|
||||||
|
const go = new Go();
|
||||||
|
self.postMessage({ type: "progress", text: "正在下载引擎", percent: 16 });
|
||||||
|
const response = await fetch("./ofdgo.wasm");
|
||||||
|
self.postMessage({ type: "progress", text: "正在编译引擎", percent: 35 });
|
||||||
|
const { instance } = await WebAssembly.instantiateStreaming(response, go.importObject);
|
||||||
|
self.postMessage({ type: "progress", text: "正在启动引擎", percent: 58 });
|
||||||
|
go.run(instance).then(exitWASM, exitWASM);
|
||||||
|
if (!go.exited) {
|
||||||
|
self.postMessage({ type: "ready" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWASM().catch(exitWASM);
|
||||||
Reference in new issue
Block a user