mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
010ab9f314
commit
66aa87304b
10 files changed
+365
-79
No files matched your search
+19
-16
@@ -4128,6 +4128,7 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) {
|
||||
}
|
||||
|
||||
function releasePageResources() {
|
||||
if (state.pageInFlight.size) return;
|
||||
const images = new Set(), fonts = new Set();
|
||||
for (const page of state.pageCache.values()) {
|
||||
for (const name of page.imageNames) images.add(name);
|
||||
@@ -4568,7 +4569,7 @@ async function loadWASM() {
|
||||
state.ready = false;
|
||||
state.wasmExited = false;
|
||||
await new Promise((resolve, reject) => {
|
||||
const worker = createEngineWorker();
|
||||
const worker = createEngineWorker({pages: true});
|
||||
wasmWorker = worker;
|
||||
const fail = (err) => {
|
||||
markWASMExited(wasmSeq, err);
|
||||
@@ -5473,6 +5474,9 @@ async function openDocument(options = {}) {
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
wasmWorker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? {
|
||||
file: state.ofdBytes, fonts, annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion
|
||||
} : null);
|
||||
if (options.keepPreview) {
|
||||
resetPageLoading();
|
||||
state.pageCache.clear();
|
||||
@@ -8724,7 +8728,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
||||
updateControls();
|
||||
}
|
||||
const result = readingSave
|
||||
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||
? { label: "OFD", size: state.ofdBytes.size ?? state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||
: saving ? await callWASM(background ? "ofdgoSaveSnapshot" : "ofdgoSaveDocument", indices, ...outputOptions, destination) : whole
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
||||
@@ -8971,10 +8975,8 @@ function resetPageFlow(keepCache = false) {
|
||||
|
||||
function resetPageLoading() {
|
||||
releaseMediaPlayers();
|
||||
if (nativeHost) {
|
||||
for (const [id, request] of wasmRequests) {
|
||||
if (request.name === "ofdgoRenderPage") wasmWorker?.postMessage({ type: "cancel", id });
|
||||
}
|
||||
for (const [id, request] of wasmRequests) {
|
||||
if (request.name === "ofdgoRenderPage") wasmWorker?.postMessage({ type: "cancel", id });
|
||||
}
|
||||
for (const task of state.pageInFlight.values()) {
|
||||
task.resolve(null);
|
||||
@@ -9088,9 +9090,10 @@ function loadPageData(index, options = {}) {
|
||||
const current = state.pageInFlight.get(key);
|
||||
if (current) {
|
||||
current.priority = Math.min(current.priority, priority);
|
||||
if (current.preview && !options.preview) return current.promise.then(() => loadPageData(index, options));
|
||||
return current.promise;
|
||||
}
|
||||
if (!options.refresh && state.pageCache.has(index)) {
|
||||
if (!options.refresh && state.pageCache.has(index) && (options.preview || !state.pageCache.get(index).previewOnly)) {
|
||||
const page = state.pageCache.get(index);
|
||||
state.pageCache.delete(index);
|
||||
state.pageCache.set(index, page);
|
||||
@@ -9102,7 +9105,7 @@ function loadPageData(index, options = {}) {
|
||||
resolve = done;
|
||||
reject = fail;
|
||||
});
|
||||
const task = { key, index, openSeq, priority, refresh: options.refresh, resolve, reject, promise };
|
||||
const task = { key, index, openSeq, priority, preview: !!options.preview, refresh: options.refresh, resolve, reject, promise };
|
||||
state.pageInFlight.set(key, task);
|
||||
state.pageRenderQueue.push(task);
|
||||
schedulePageRender();
|
||||
@@ -9112,7 +9115,7 @@ function loadPageData(index, options = {}) {
|
||||
function schedulePageRender() {
|
||||
if (state.pageRenderRunning) {
|
||||
if (state.pageRenderWorkers) {
|
||||
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || 1)) processPageRenderTasks();
|
||||
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || wasmWorker?.parallelism || 1)) processPageRenderTasks();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -9122,7 +9125,7 @@ function schedulePageRender() {
|
||||
|
||||
async function processPageRenderQueue() {
|
||||
await nextFrame();
|
||||
await Promise.all(Array.from({ length: nativeHost?.parallelism || 1 }, () => processPageRenderTasks()));
|
||||
await Promise.all(Array.from({ length: nativeHost?.parallelism || wasmWorker?.parallelism || 1 }, () => processPageRenderTasks()));
|
||||
}
|
||||
|
||||
async function processPageRenderTasks() {
|
||||
@@ -9139,12 +9142,12 @@ async function processPageRenderTasks() {
|
||||
task.resolve(null);
|
||||
continue;
|
||||
}
|
||||
if (!task.refresh && state.pageCache.has(task.index)) {
|
||||
if (!task.refresh && state.pageCache.has(task.index) && (task.preview || !state.pageCache.get(task.index).previewOnly)) {
|
||||
task.resolve(state.pageCache.get(task.index));
|
||||
continue;
|
||||
}
|
||||
const args = [task.index, state.renderBackend, state.renderDPI, displayMode() === "raster"];
|
||||
if (nativeHost) args.push([...state.svgFonts.keys()]);
|
||||
args.push([...state.svgFonts.keys()], [...state.svgImages.keys()], task.preview);
|
||||
const page = await callWASM("ofdgoRenderPage", ...args);
|
||||
if (state.pageInFlight.get(task.key) !== task) {
|
||||
task.resolve(null);
|
||||
@@ -9230,7 +9233,7 @@ function trimPageCache() {
|
||||
for (const name of page.imageNames) {
|
||||
const count = references.get(name) - 1;
|
||||
references.set(name, count);
|
||||
if (count === 0) {
|
||||
if (count === 0 && !state.pageInFlight.size) {
|
||||
const image = state.svgImages.get(name);
|
||||
bytes -= image.size;
|
||||
URL.revokeObjectURL(image.url);
|
||||
@@ -11197,7 +11200,7 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
|
||||
}
|
||||
state.thumbnailInFlight.add(key);
|
||||
try {
|
||||
const page = await loadPageData(index, { openSeq, priority: 5 });
|
||||
const page = await loadPageData(index, { openSeq, priority: 5, preview: true });
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
@@ -12547,11 +12550,11 @@ async function callWASM(name, ...args) {
|
||||
}
|
||||
try {
|
||||
const transfer = [];
|
||||
if (name === "ofdgoOpen" && !nativeHost) {
|
||||
if (name === "ofdgoOpen" && !nativeHost && ArrayBuffer.isView(args[0])) {
|
||||
args[0] = args[0].slice();
|
||||
transfer.push(args[0].buffer);
|
||||
}
|
||||
if (name === "ofdgoLoadImport" && args[0] && !nativeHost) transfer.push(args[0].buffer);
|
||||
if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer);
|
||||
wasmWorker.postMessage({ id, name, args }, transfer);
|
||||
} catch (err) {
|
||||
wasmRequests.delete(id);
|
||||
|
||||
+152
-2
@@ -1,7 +1,8 @@
|
||||
let engineModule = null;
|
||||
|
||||
export function createEngineWorker() {
|
||||
export function createEngineWorker(options = {}) {
|
||||
if (nativeHost) return nativeHost.worker();
|
||||
if (options.pages) return new BrowserEngine();
|
||||
const worker = new Worker("./ofdgo_wasm.js");
|
||||
worker.addEventListener("message", event => {
|
||||
if (event.data.type !== "module") return;
|
||||
@@ -13,7 +14,156 @@ export function createEngineWorker() {
|
||||
}
|
||||
|
||||
export async function documentBytes(file) {
|
||||
return file?.nativeFile ? file : new Uint8Array(await file.arrayBuffer());
|
||||
if (file?.nativeFile) return file;
|
||||
if (file instanceof Blob) return new Response(file.stream()).blob();
|
||||
return new Uint8Array(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
class BrowserEngine {
|
||||
constructor() {
|
||||
this.primary = createEngineWorker();
|
||||
this.primary.onmessage = event => {
|
||||
const data = event.data;
|
||||
if (data.memory) this.memory = data.memory;
|
||||
if (data.id != null && !data.type) this.requests.delete(data.id);
|
||||
this.onmessage?.(event);
|
||||
};
|
||||
this.primary.onerror = event => this.onerror?.(event);
|
||||
this.primary.onmessageerror = event => this.onmessageerror?.(event);
|
||||
this.requests = new Map();
|
||||
this.slots = new Set();
|
||||
this.sequence = 0;
|
||||
this.revision = 0;
|
||||
this.memory = 0;
|
||||
this.parallelism = 1;
|
||||
this.budget = Math.max(128, Math.min(1024, (navigator.deviceMemory || 2) * 128)) * 1024 * 1024;
|
||||
this.closed = false;
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
configurePages(source) {
|
||||
this.invalidatePages();
|
||||
if (!source || this.closed) return;
|
||||
this.source = {...source, file: source.file instanceof Blob ? source.file : new Blob([source.file])};
|
||||
this.parallelism = Math.max(1, Math.min(2, (navigator.hardwareConcurrency || 2) - 1, Math.floor(this.budget / (256 * 1024 * 1024))));
|
||||
}
|
||||
|
||||
invalidatePages() {
|
||||
this.revision++;
|
||||
this.source = null;
|
||||
this.parallelism = 1;
|
||||
for (const slot of this.slots) this.closeSlot(slot);
|
||||
}
|
||||
|
||||
closeSlot(slot, error = new DOMException("页面加载已取消", "AbortError")) {
|
||||
if (!this.slots.delete(slot)) return;
|
||||
slot.worker.terminate();
|
||||
slot.rejectReady(error);
|
||||
for (const pending of slot.pending.values()) pending.reject(error);
|
||||
slot.pending.clear();
|
||||
}
|
||||
|
||||
createSlot(source) {
|
||||
const worker = createEngineWorker();
|
||||
const slot = {worker, pending: new Map(), busy: false, memory: Math.max(128 << 20, this.memory)};
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
slot.rejectReady = reject;
|
||||
worker.onmessage = ({data}) => {
|
||||
if (data.memory) slot.memory = data.memory;
|
||||
if (data.type === "ready") { resolve(); return; }
|
||||
if (data.type === "exit") { this.closeSlot(slot, new Error("页面加载失败")); return; }
|
||||
if (data.type) return;
|
||||
const request = slot.pending.get(data.id);
|
||||
if (!request) return;
|
||||
slot.pending.delete(data.id);
|
||||
if (data.ok) request.resolve(data);
|
||||
else request.reject(Object.assign(new Error(data.error), {code: data.code}));
|
||||
};
|
||||
worker.onerror = worker.onmessageerror = () => this.closeSlot(slot, new Error("页面加载失败"));
|
||||
});
|
||||
this.slots.add(slot);
|
||||
slot.ready = ready.then(async () => {
|
||||
let opened = await this.callSlot(slot, "ofdgoOpen", [source.file, source.fonts, source.annotations]);
|
||||
if ((source.document || 0) !== (opened.data.documentIndex || 0)) opened = await this.callSlot(slot, "ofdgoSelectDocument", [source.document]);
|
||||
if ((source.version || "") !== (opened.data.documentVersion || "")) await this.callSlot(slot, "ofdgoSelectVersion", [source.version || ""]);
|
||||
});
|
||||
slot.ready.catch(() => {});
|
||||
return slot;
|
||||
}
|
||||
|
||||
callSlot(slot, name, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.slots.has(slot)) { reject(new DOMException("页面加载已取消", "AbortError")); return; }
|
||||
const id = --this.sequence;
|
||||
slot.pending.set(id, {resolve, reject});
|
||||
try { slot.worker.postMessage({id, name, args}); }
|
||||
catch (error) { slot.pending.delete(id); reject(error); }
|
||||
});
|
||||
}
|
||||
|
||||
postMessage(data, transfer) {
|
||||
if (this.closed) return;
|
||||
if (data.type === "cancel") {
|
||||
const slot = this.requests.get(data.id);
|
||||
if (slot) this.closeSlot(slot);
|
||||
else this.primary.postMessage(data);
|
||||
return;
|
||||
}
|
||||
if (!data.type && data.name !== "ofdgoRenderPage" && ![
|
||||
"ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData",
|
||||
"ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets",
|
||||
"ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles",
|
||||
"ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures",
|
||||
"ofdgoAnnotationInfos", "ofdgoInvoices", "ofdgoInvoice", "ofdgoExportInvoice", "ofdgoValidateInvoice",
|
||||
"ofdgoActionList", "ofdgoPageSettings", "ofdgoReadAnnotation", "ofdgoPreviewSignatureSeal",
|
||||
"ofdgoExportFormats", "ofdgoOutputFormats", "ofdgoParsePageRange", "ofdgoExportPage", "ofdgoExportDocument",
|
||||
"ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion"
|
||||
].includes(data.name)) this.invalidatePages();
|
||||
if (data.name !== "ofdgoRenderPage" || !this.source || ![...this.requests.values()].includes(null)) {
|
||||
if (data.name === "ofdgoRenderPage") this.requests.set(data.id, null);
|
||||
this.primary.postMessage(data, transfer);
|
||||
return;
|
||||
}
|
||||
let slot = [...this.slots].find(slot => !slot.busy);
|
||||
const memory = this.memory + [...this.slots].reduce((total, item) => total + item.memory, 0);
|
||||
if (!slot && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
|
||||
if (!slot) {
|
||||
this.requests.set(data.id, null);
|
||||
this.primary.postMessage(data, transfer);
|
||||
return;
|
||||
}
|
||||
slot.busy = true;
|
||||
this.requests.set(data.id, slot);
|
||||
const revision = this.revision;
|
||||
let fallback = false;
|
||||
slot.ready.then(() => this.callSlot(slot, data.name, data.args)).then(result => {
|
||||
if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError");
|
||||
if (!this.closed) this.onmessage?.({data: {...result, id: data.id}});
|
||||
}).catch(error => {
|
||||
if (!this.closed && revision === this.revision && error.name !== "AbortError") {
|
||||
this.closeSlot(slot, error);
|
||||
this.parallelism = Math.max(1, this.slots.size + 1);
|
||||
this.requests.set(data.id, null);
|
||||
fallback = true;
|
||||
this.primary.postMessage(data, transfer);
|
||||
return;
|
||||
}
|
||||
if (!this.closed) this.onmessage?.({data: {id: data.id, ok: false, error: error.message, canceled: error.name === "AbortError"}});
|
||||
}).finally(() => {
|
||||
if (!fallback) this.requests.delete(data.id);
|
||||
slot.busy = false;
|
||||
const memory = this.memory + [...this.slots].reduce((total, item) => total + item.memory, 0);
|
||||
if (memory > this.budget) { this.closeSlot(slot); this.parallelism = Math.max(1, this.slots.size + 1); }
|
||||
});
|
||||
}
|
||||
|
||||
terminate() {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
this.invalidatePages();
|
||||
this.primary.terminate();
|
||||
this.requests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function saveFilePicker(options) {
|
||||
|
||||
@@ -190,6 +190,10 @@ async function handleMessage({ id, name, args }) {
|
||||
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
if (["ofdgoOpen", "ofdgoLoadImport", "ofdgoConvertPDF"].includes(name) && args[0] instanceof Blob) {
|
||||
const file = args[0], reader = new FileReaderSync();
|
||||
args[0] = {size: file.size, read: (offset, size) => new Uint8Array(reader.readAsArrayBuffer(file.slice(offset, offset + size)))};
|
||||
}
|
||||
if (name === "ofdgoDetectFormat") {
|
||||
const file = args[0], reader = new FileReaderSync();
|
||||
channel = new MessageChannel();
|
||||
@@ -299,8 +303,8 @@ async function handleMessage({ id, name, args }) {
|
||||
await output.abort();
|
||||
}
|
||||
output = null;
|
||||
const transfer = result.data?.bytes ? [result.data.bytes.buffer] : (result.data?.images || []).map((image) => image.bytes.buffer);
|
||||
self.postMessage({ id, ...result }, transfer);
|
||||
const transfer = [...new Set([result.data?.bytes, ...(result.data?.images || []).map(image => image.bytes), ...(result.data?.fonts || []).map(font => font.data)].filter(bytes => bytes?.byteLength).map(bytes => bytes.buffer))];
|
||||
self.postMessage({ id, ...result, memory: engineMemory.buffer.byteLength }, transfer);
|
||||
} catch (err) {
|
||||
await output?.abort().catch(() => {});
|
||||
const result = { id, ok: false, error: err.message, errorName: err.name };
|
||||
|
||||
Reference in new issue
Block a user