mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
ea55e997ee
commit
83d936e9f8
8 files changed
+357
-41
No files matched your search
+52
-17
@@ -133,12 +133,14 @@ const state = {
|
||||
pageCache: new Map(),
|
||||
mediaPlayers: new Map(),
|
||||
svgFonts: new Map(),
|
||||
svgFontLoads: new Map(),
|
||||
svgImages: new Map(),
|
||||
selectedPages: new Set(),
|
||||
documentSelection: null,
|
||||
pageInFlight: new Map(),
|
||||
pageRenderQueue: [],
|
||||
pageRenderRunning: false,
|
||||
pageRenderWorkers: 0,
|
||||
pageObserver: null,
|
||||
visiblePages: new Set(),
|
||||
scrollFrame: 0,
|
||||
@@ -8969,6 +8971,11 @@ 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 task of state.pageInFlight.values()) {
|
||||
task.resolve(null);
|
||||
}
|
||||
@@ -9104,6 +9111,9 @@ function loadPageData(index, options = {}) {
|
||||
|
||||
function schedulePageRender() {
|
||||
if (state.pageRenderRunning) {
|
||||
if (state.pageRenderWorkers) {
|
||||
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || 1)) processPageRenderTasks();
|
||||
}
|
||||
return;
|
||||
}
|
||||
state.pageRenderRunning = true;
|
||||
@@ -9111,8 +9121,13 @@ function schedulePageRender() {
|
||||
}
|
||||
|
||||
async function processPageRenderQueue() {
|
||||
await nextFrame();
|
||||
await Promise.all(Array.from({ length: nativeHost?.parallelism || 1 }, () => processPageRenderTasks()));
|
||||
}
|
||||
|
||||
async function processPageRenderTasks() {
|
||||
state.pageRenderWorkers++;
|
||||
try {
|
||||
await nextFrame();
|
||||
while (state.pageRenderQueue.length) {
|
||||
let next = 0;
|
||||
for (let index = 1; index < state.pageRenderQueue.length; index++) {
|
||||
@@ -9128,7 +9143,9 @@ async function processPageRenderQueue() {
|
||||
task.resolve(state.pageCache.get(task.index));
|
||||
continue;
|
||||
}
|
||||
const page = await callWASM("ofdgoRenderPage", task.index, state.renderBackend, state.renderDPI, displayMode() === "raster");
|
||||
const args = [task.index, state.renderBackend, state.renderDPI, displayMode() === "raster"];
|
||||
if (nativeHost) args.push([...state.svgFonts.keys()]);
|
||||
const page = await callWASM("ofdgoRenderPage", ...args);
|
||||
if (state.pageInFlight.get(task.key) !== task) {
|
||||
task.resolve(null);
|
||||
continue;
|
||||
@@ -9139,6 +9156,7 @@ async function processPageRenderQueue() {
|
||||
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
||||
}
|
||||
await loadSVGFonts(page.fonts, task.openSeq);
|
||||
for (const font of page.fonts) delete font.data;
|
||||
if (task.openSeq === state.openSeq && state.pageInFlight.get(task.key) === task) {
|
||||
for (const image of page.images) {
|
||||
if (!state.svgImages.has(image.name)) {
|
||||
@@ -9172,8 +9190,11 @@ async function processPageRenderQueue() {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
state.pageRenderRunning = false;
|
||||
if (!state.pageInFlight.size) releasePageResources();
|
||||
if (--state.pageRenderWorkers === 0) {
|
||||
state.pageRenderRunning = false;
|
||||
if (!state.pageInFlight.size) releasePageResources();
|
||||
if (state.pageRenderQueue.length) schedulePageRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9196,8 +9217,9 @@ function trimPageCache() {
|
||||
for (const image of state.svgImages.values()) {
|
||||
bytes += image.size;
|
||||
}
|
||||
const budget = nativeHost?.cacheBytes || PAGE_CACHE_BYTES;
|
||||
for (const [index, page] of state.pageCache) {
|
||||
if (state.pageCache.size <= PAGE_CACHE_LIMIT && bytes <= PAGE_CACHE_BYTES) {
|
||||
if (state.pageCache.size <= PAGE_CACHE_LIMIT * Math.max(1, budget / PAGE_CACHE_BYTES) && bytes <= budget) {
|
||||
break;
|
||||
}
|
||||
if (protectedPages.has(index)) {
|
||||
@@ -9227,20 +9249,31 @@ async function loadSVGFonts(fonts, openSeq) {
|
||||
if (state.svgFonts.has(font.name)) {
|
||||
continue;
|
||||
}
|
||||
const data = await callWASM("ofdgoSVGFontData", font.name);
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
const key = `${openSeq}:${font.name}`;
|
||||
let loading = state.svgFontLoads.get(key);
|
||||
if (!loading) {
|
||||
loading = loadSVGFont(font, openSeq);
|
||||
state.svgFontLoads.set(key, loading);
|
||||
}
|
||||
const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style });
|
||||
await face.load();
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
document.fonts.add(face);
|
||||
state.svgFonts.set(font.name, face);
|
||||
try { await loading; }
|
||||
finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); }
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSVGFont(font, openSeq) {
|
||||
const data = font.data ? { bytes: font.data } : await callWASM("ofdgoSVGFontData", font.name);
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style });
|
||||
await face.load();
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
document.fonts.add(face);
|
||||
state.svgFonts.set(font.name, face);
|
||||
}
|
||||
|
||||
function comparePageRenderTask(a, b) {
|
||||
return a.priority - b.priority || Math.abs(a.index - state.pageIndex) - Math.abs(b.index - state.pageIndex);
|
||||
}
|
||||
@@ -9853,7 +9886,9 @@ function markFlowPageError(index, err) {
|
||||
|
||||
function queueNearbyPages(index, openSeq = state.openSeq) {
|
||||
const start = pageGroupStart(index);
|
||||
for (let i = Math.max(0, start - 1); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + 1); i += 1) {
|
||||
const before = nativeHost ? state.pageLayout : 1;
|
||||
const after = nativeHost ? Math.max(1, Math.min(nativeHost.parallelism, state.pageLayout * 2)) : 1;
|
||||
for (let i = Math.max(0, start - before); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + after); i += 1) {
|
||||
renderFlowPage(i, { openSeq, priority: 2 });
|
||||
}
|
||||
}
|
||||
@@ -12497,7 +12532,7 @@ async function callWASM(name, ...args) {
|
||||
el.cancelExportButton.disabled = false;
|
||||
}
|
||||
if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id;
|
||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||
wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq });
|
||||
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
|
||||
if (name === "ofdgoConvertPDF") {
|
||||
const request = wasmRequests.get(id);
|
||||
|
||||
+47
-20
@@ -87,6 +87,10 @@ class NativeHandle {
|
||||
class NativeHost {
|
||||
constructor(bridge) {
|
||||
this.bridge = bridge;
|
||||
this.parallelism = 1;
|
||||
this.cacheBytes = 32 * 1024 * 1024;
|
||||
this.reading = 0;
|
||||
this.readQueue = [];
|
||||
this.resources = typeof FinalizationRegistry === "function" ? new FinalizationRegistry(file => {
|
||||
this.call("release", {file}).catch(() => {});
|
||||
}) : null;
|
||||
@@ -96,25 +100,40 @@ class NativeHost {
|
||||
return this.bridge.call(`ofdgo.host.${name}`, value, {signal});
|
||||
}
|
||||
|
||||
async decode(value, name = "", key = "", signal) {
|
||||
if (value?.nativeFile) {
|
||||
const file = new NativeFile(value);
|
||||
if (key === "file" || name === "ofdgoConvertPDF" && key === "bytes") return file;
|
||||
try {
|
||||
const bytes = await file.arrayBuffer(signal);
|
||||
return key === "blob" ? new Blob([bytes], {type:value.type}) : new Uint8Array(bytes);
|
||||
} finally {
|
||||
await this.call("release", {file:value.nativeFile});
|
||||
async readResource(file, signal) {
|
||||
if (this.reading >= this.parallelism) await new Promise(resolve => this.readQueue.push(resolve));
|
||||
else this.reading++;
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
return await file.arrayBuffer(signal);
|
||||
} finally {
|
||||
try { await this.call("release", {file:file.nativeFile}); }
|
||||
finally {
|
||||
const next = this.readQueue.shift();
|
||||
if (next) next();
|
||||
else this.reading--;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) value[i] = await this.decode(value[i], name, "", signal);
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
for (const [field, item] of Object.entries(value)) value[field] = await this.decode(item, name, field, signal);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async decode(value, name = "", key = "", signal) {
|
||||
const reads = [];
|
||||
const visit = (item, field, replace) => {
|
||||
if (!item || typeof item !== "object" || item instanceof Blob || item instanceof ArrayBuffer || ArrayBuffer.isView(item)) return;
|
||||
if (item.nativeFile) {
|
||||
const file = new NativeFile(item);
|
||||
if (field === "file" || name === "ofdgoConvertPDF" && field === "bytes") replace(file);
|
||||
else reads.push(this.readResource(file, signal).then(bytes => replace(field === "blob" ? new Blob([bytes], {type:item.type}) : new Uint8Array(bytes))));
|
||||
return;
|
||||
}
|
||||
for (const [child, content] of Object.entries(item)) {
|
||||
if (content && typeof content === "object") visit(content, Array.isArray(item) ? "" : child, result => item[child] = result);
|
||||
}
|
||||
};
|
||||
visit(value, key, result => value = result);
|
||||
const results = await Promise.allSettled(reads);
|
||||
const failed = results.find(result => result.status === "rejected");
|
||||
if (failed) throw failed.reason;
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -134,14 +153,17 @@ class NativeHost {
|
||||
let closed = false;
|
||||
let unsubscribe;
|
||||
let pending = Promise.resolve();
|
||||
const readers = new Set();
|
||||
const requests = new Map();
|
||||
const completions = new Map();
|
||||
const emit = data => {
|
||||
if (data.type === "result") { completions.get(data.id)?.(data.result); return; }
|
||||
if (!closed) worker.onmessage?.({data});
|
||||
};
|
||||
const ready = host.call("engine.open", {}).then(({engine}) => {
|
||||
const ready = host.call("engine.open", {}).then(({engine, parallelism, cacheBytes}) => {
|
||||
if (closed) return engine;
|
||||
host.parallelism = Math.max(1, parallelism || 1);
|
||||
host.cacheBytes = cacheBytes || host.cacheBytes;
|
||||
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
||||
emit({type:"ready"});
|
||||
return engine;
|
||||
@@ -158,7 +180,9 @@ class NativeHost {
|
||||
if (data.type === "font") return;
|
||||
const controller = new AbortController();
|
||||
requests.set(data.id, controller);
|
||||
const operation = pending.then(async () => {
|
||||
const parallel = data.name === "ofdgoRenderPage";
|
||||
const barrier = parallel ? pending : Promise.all([pending, ...readers]);
|
||||
const operation = barrier.then(async () => {
|
||||
const engine = await ready;
|
||||
if (closed) return;
|
||||
controller.signal.throwIfAborted();
|
||||
@@ -189,7 +213,10 @@ class NativeHost {
|
||||
if (closed) { await host.release(result); return; }
|
||||
emit({id:data.id, ...decoded});
|
||||
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
||||
if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
||||
if (parallel) {
|
||||
readers.add(operation);
|
||||
operation.finally(() => readers.delete(operation));
|
||||
} else if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
||||
},
|
||||
terminate() {
|
||||
if (closed) return;
|
||||
|
||||
Reference in new issue
Block a user