Compare commits

...
3 Commits
Author SHA1 Message Date
xiaoqidun fb4f03465b feat(强化加载): 支持PDF按页加载与增量转换
build.yaml / build (push) Successful in 44s
2026-10-26 17:21:15 +08:00
xiaoqidun 04d7d2d672 perf(性能提升): 优化字体并发与复杂页渲染 2026-10-26 14:42:35 +08:00
xiaoqidun 15a532a1ca perf(性能提升): 优化字体加载与页面渲染调度 2026-10-26 14:17:22 +08:00
11 changed files with 747 additions and 198 deletions

No files matched your search

+50 -28
View File
@@ -116,6 +116,8 @@ function createDocumentState() {
textFontID: null, textFontID: null,
textDefaults: { type: "TextObject", size: 16 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 }, textDefaults: { type: "TextObject", size: 16 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
ofdBytes: null, ofdBytes: null,
sourcePDF: false,
pdfComplete: false,
fileName: "ofdgo.ofd", fileName: "ofdgo.ofd",
openSeq: 0, openSeq: 0,
pageSeq: 0, pageSeq: 0,
@@ -5503,11 +5505,10 @@ async function loadWASM(tab = documentTab()) {
if (data.type === "progress") { if (data.type === "progress") {
if (visible) setProgress(data.text, data.percent); if (visible) setProgress(data.text, data.percent);
} else if (data.type === "conversion") { } else if (data.type === "conversion") {
if (visible && wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) { if (visible && wasmRequests.get(data.id)?.name !== "ofdgoLoadPDF" && wasmRequests.get(data.id)?.openSeq === state.openSeq && !el.cancelExportButton.disabled) {
if (data.phase === "open") setProgress("正在读取文档", null); if (data.phase === "open") setProgress("正在读取文档", null);
else if (data.phase === "pages") setProgress(data.completed ? `正在整理页面 · ${data.completed} 页` : "正在整理页面", null); else if (data.phase === "pages") setProgress(data.completed ? `正在整理页面 · ${data.completed} 页` : "正在整理页面", null);
else if (data.phase === "convert") setProgress(`正在转换页面 · ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null); else if (data.phase === "convert") setProgress(`正在转换页面 · ${data.completed} / ${data.total} 页`, data.total ? 28 + data.completed / data.total * 22 : null);
else if (data.phase === "write.fonts") setProgress("正在处理字体", null);
else setProgress("正在生成文档", null); else setProgress("正在生成文档", null);
} }
} else if (data.type === "export") { } else if (data.type === "export") {
@@ -5723,7 +5724,6 @@ async function openOFD(file, remote = null, handle = file?.handle) {
workspace.tabs.push(tab); workspace.tabs.push(tab);
const current = () => !tab.closed && tab.opening === request; const current = () => !tab.closed && tab.opening === request;
let accepted = false; let accepted = false;
let originalSource = null;
try { try {
setBusy(true, "正在读取文档", 10, STATUS.opening, tab); setBusy(true, "正在读取文档", 10, STATUS.opening, tab);
await activateDocumentTab(tab); await activateDocumentTab(tab);
@@ -5742,11 +5742,10 @@ async function openOFD(file, remote = null, handle = file?.handle) {
const source = downloaded ? downloaded.file : file; const source = downloaded ? downloaded.file : file;
const format = await callTabWASM(tab, "ofdgoDetectFormat", source); const format = await callTabWASM(tab, "ofdgoDetectFormat", source);
if (!current()) return; if (!current()) return;
let bytes = await documentBytes(source); const bytes = await documentBytes(source);
originalSource = bytes;
tab.state.ofdBytes = bytes; tab.state.ofdBytes = bytes;
tab.state.sourcePDF = format === "pdf";
const name = (downloaded ? downloaded.name : file.name) || `ofdgo.${format}`; const name = (downloaded ? downloaded.name : file.name) || `ofdgo.${format}`;
let warnings = [];
let convertedDoc; let convertedDoc;
if (!current()) { if (!current()) {
return; return;
@@ -5765,10 +5764,7 @@ async function openOFD(file, remote = null, handle = file?.handle) {
} }
} }
if (!current()) return; if (!current()) return;
bytes = converted.bytes; convertedDoc = converted;
tab.state.ofdBytes = bytes;
convertedDoc = converted.doc;
warnings = JSON.parse(converted.warnings || "null") || [];
} }
await ensureTabEngine(tab); await ensureTabEngine(tab);
if (!current()) return; if (!current()) return;
@@ -5776,7 +5772,7 @@ async function openOFD(file, remote = null, handle = file?.handle) {
if (!current()) return; if (!current()) return;
tab.state.saveHandle = format === "ofd" && !remote ? handle || null : null; tab.state.saveHandle = format === "ofd" && !remote ? handle || null : null;
tab.state.saveStamp = tab.state.saveHandle && !nativeHost ? {size: file.size, modified: file.lastModified} : null; tab.state.saveStamp = tab.state.saveHandle && !nativeHost ? {size: file.size, modified: file.lastModified} : null;
tab.state.conversionWarnings = warnings; tab.state.conversionWarnings = [];
tab.state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd"); tab.state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd");
if (remote && !/\.ofd$/i.test(tab.state.fileName)) tab.state.fileName += ".ofd"; if (remote && !/\.ofd$/i.test(tab.state.fileName)) tab.state.fileName += ".ofd";
tab.state.doc = doc; tab.state.doc = doc;
@@ -5791,6 +5787,7 @@ async function openOFD(file, remote = null, handle = file?.handle) {
if (isForegroundDocument(tab) && openSeq === tab.state.openSeq) tab.initialLoad = false; if (isForegroundDocument(tab) && openSeq === tab.state.openSeq) tab.initialLoad = false;
} }
else tab.status = "文档已打开"; else tab.status = "文档已打开";
if (tab.state.sourcePDF) completePDF(tab);
} catch (err) { } catch (err) {
if (current()) { if (current()) {
tab.error = err.name === "AbortError" ? "打开已取消" : err.message; tab.error = err.name === "AbortError" ? "打开已取消" : err.message;
@@ -5807,7 +5804,6 @@ async function openOFD(file, remote = null, handle = file?.handle) {
if (nativeHost) await nativeHost.release(source); if (nativeHost) await nativeHost.release(source);
tab.state.ofdBytes = null; tab.state.ofdBytes = null;
} }
if (nativeHost && originalSource !== source) await nativeHost.release(originalSource);
} }
return accepted; return accepted;
} }
@@ -6313,22 +6309,15 @@ function requestCredentials(code, openSeq, pdf = false, tab = documentTab()) {
async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false, pdf = false, tab = documentTab()) { async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false, pdf = false, tab = documentTab()) {
const state = tab.state; const state = tab.state;
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args); const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
pdf ||= !importing && state.sourcePDF;
const current = () => !tab.closed && (Boolean(tab.opening) || openSeq === state.openSeq); const current = () => !tab.closed && (Boolean(tab.opening) || openSeq === state.openSeq);
let credentials = null; let credentials = null;
try { try {
while (current()) { while (current()) {
try { try {
if (pdf) { if (pdf) {
const converted = await callWASM("ofdgoConvertPDF", data, fonts, credentials?.password || null); state.pdfComplete = false;
let accepted = false; return await callWASM("ofdgoOpenPDF", data, fonts, credentials?.password || null, state.renderAnnotations);
try {
if (!current()) return converted;
converted.doc = await callWASM("ofdgoOpen", converted.bytes, fonts, state.renderAnnotations, credentials);
accepted = current();
return converted;
} finally {
if (!accepted && nativeHost) await nativeHost.release(converted.bytes);
}
} }
return importing ? await callWASM("ofdgoLoadImport", data?.nativeFile ? data : data.slice(), credentials) return importing ? await callWASM("ofdgoLoadImport", data?.nativeFile ? data : data.slice(), credentials)
: await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials); : await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials);
@@ -6444,8 +6433,10 @@ async function openDocument(options = {}) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
wasmWorker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? { const pageFonts = state.sourcePDF && !nativeHost && !state.editorInfo ? await fontManager.files(fontManager.records()) : fonts;
file: state.ofdBytes, fonts, annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion if (openSeq !== state.openSeq) return;
tab.worker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? {
file: state.ofdBytes, fonts: pageFonts, pdf: state.sourcePDF, fontNames: state.sourcePDF ? [...fontManager.localEntries().keys()] : [], annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion
} : null); } : null);
if (options.keepPreview) { if (options.keepPreview) {
pageView.resetPageLoading(); pageView.resetPageLoading();
@@ -6535,10 +6526,40 @@ async function openDocument(options = {}) {
} }
} }
async function completePDF(tab) {
const state = tab.state, source = state.ofdBytes, wasmSeq = state.wasmSeq;
if (tab.pdfTask?.wasmSeq === wasmSeq || state.pdfComplete || !state.sourcePDF || !source || state.editorInfo) return;
const task = {wasmSeq};
tab.pdfTask = task;
const current = () => !tab.closed && state.ofdBytes === source && state.wasmSeq === wasmSeq;
try {
while (current()) {
await new Promise(resolve => setTimeout(resolve, 0));
if (!current()) return;
const result = await callTabWASM(tab, "ofdgoLoadPDF");
if (!current()) return;
if (!result.done) continue;
state.pdfComplete = true;
state.conversionWarnings = JSON.parse(result.warnings || "null") || [];
if (!state.editorInfo) Object.assign(state.doc, result.doc);
if (isForegroundDocument(tab)) loadDocumentDetails(state.openSeq);
return;
}
} catch (err) {
if (current() && !err.canceled) {
tab.status = err.message;
if (isForegroundDocument(tab)) showError(err, false);
}
} finally {
if (tab.pdfTask === task) tab.pdfTask = null;
}
}
async function loadDocumentDetails(openSeq) { async function loadDocumentDetails(openSeq) {
const tab = documentTab(), state = tab.state; const tab = documentTab(), state = tab.state;
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args); const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
if (openSeq !== state.openSeq || !state.doc) return; if (openSeq !== state.openSeq || !state.doc) return;
if (state.sourcePDF && !state.pdfComplete && !state.editorInfo) { completePDF(tab); return; }
try { try {
const details = await callWASM("ofdgoDocumentInfo"); const details = await callWASM("ofdgoDocumentInfo");
if (openSeq !== state.openSeq) return; if (openSeq !== state.openSeq) return;
@@ -9930,7 +9951,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
if (tab.closed || wasmSeq !== state.wasmSeq) return; if (tab.closed || wasmSeq !== state.wasmSeq) return;
setBusy(true, "正在生成文件", null, status, tab); setBusy(true, "正在生成文件", null, status, tab);
} }
const readingSave = saving && !state.editorInfo && indices === null && compression.mode === 0; const readingSave = saving && !state.sourcePDF && !state.editorInfo && indices === null && compression.mode === 0;
const destination = !nativeHost && file && file === state.saveHandle && state.saveStamp ? {fileHandle: file, saveStamp: state.saveStamp} : file; const destination = !nativeHost && file && file === state.saveHandle && state.saveStamp ? {fileHandle: file, saveStamp: state.saveStamp} : file;
if (saving && indices === null && !readingSave) { if (saving && indices === null && !readingSave) {
snapshotPrepared = true; snapshotPrepared = true;
@@ -12350,13 +12371,13 @@ async function callTabWASM(tab, name, ...args) {
if (name === "ofdgoResources" && workspace.active === tab && resourceElements.Panel.open) resourceManager.requestID = id; if (name === "ofdgoResources" && workspace.active === tab && resourceElements.Panel.open) resourceManager.requestID = id;
wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq, tab }); wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq, tab });
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0]; if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
if (name === "ofdgoConvertPDF") { if (name === "ofdgoOpenPDF" || state.sourcePDF && !state.editorInfo) {
const request = wasmRequests.get(id); const request = wasmRequests.get(id);
request.localFonts = fontManager.localEntries(); request.localFonts = fontManager.localEntries();
request.fontData = new Map(); request.fontData = new Map();
args.push([...request.localFonts.keys()]); if (name === "ofdgoOpenPDF") args.push([...request.localFonts.keys()]);
} }
if (name === "ofdgoImportDocuments" || name === "ofdgoImportImages" || name === "ofdgoExportImages" || name === "ofdgoConvertPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoExportResource" || name === "ofdgoExportVersion" || name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") { if (name === "ofdgoImportDocuments" || name === "ofdgoImportImages" || name === "ofdgoExportImages" || name === "ofdgoOpenPDF" || name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoExportResource" || name === "ofdgoExportVersion" || name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
state.exportRequestID = id; state.exportRequestID = id;
if (visible) { if (visible) {
el.cancelExportButton.hidden = false; el.cancelExportButton.hidden = false;
@@ -12371,6 +12392,7 @@ async function callTabWASM(tab, name, ...args) {
} }
if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer); if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer);
const message = {id, name, args}; const message = {id, name, args};
if (name === "ofdgoLoadPDF") message.background = true;
if (name === "ofdgoRenderPage" && [...state.pageInFlight.values()].some(task => task.index === args[0] && pageView.pageRenderPriority(task, state) >= 6)) message.preload = true; if (name === "ofdgoRenderPage" && [...state.pageInFlight.values()].some(task => task.index === args[0] && pageView.pageRenderPriority(task, state) >= 6)) message.preload = true;
tab.worker.postMessage(message, transfer); tab.worker.postMessage(message, transfer);
} catch (err) { } catch (err) {
+93 -25
View File
@@ -35,7 +35,7 @@ class BrowserScheduler {
} }
priority(task) { priority(task) {
return Number(task.worker.batch || this.owner(task.worker, task.data)?.foreground === false) + Number(!!task.data.preload); return Number(task.worker.batch || this.owner(task.worker, task.data)?.foreground === false) + Number(!!task.data.preload || !!task.data.background);
} }
submit(worker, data, transfer) { submit(worker, data, transfer) {
@@ -64,10 +64,16 @@ class BrowserScheduler {
if (data.id == null || data.type) return; if (data.id == null || data.type) return;
const key = requestKey(worker, data); const key = requestKey(worker, data);
const started = worker.requests.get(key); const started = worker.requests.get(key);
const elapsed = data.elapsed ?? (started === undefined ? 0 : performance.now() - started);
const operation = worker.operations.get(key), owner = this.owner(worker, data);
if (worker.primary && started !== undefined && data.ok && owner) {
if (operation?.name === "ofdgoOpen") owner.openTime = elapsed;
if (operation?.name === "ofdgoRenderPage") owner.pageTime = owner.pageTime ? owner.pageTime * 0.75 + elapsed * 0.25 : elapsed;
}
worker.requests.delete(key); worker.requests.delete(key);
worker.operations.delete(key); worker.operations.delete(key);
if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1); if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1);
else if (started !== undefined && data.ok && performance.now() - started > 100 && this.memory + Math.max(128 << 20, worker.memory) < this.budget) this.limit = Math.min(this.capacity, this.limit + 1); else if (started !== undefined && data.ok && elapsed > 100 && this.memory + Math.max(128 << 20, worker.memory) < this.budget) this.limit = Math.min(this.capacity, this.limit + 1);
} }
drain() { drain() {
@@ -86,8 +92,8 @@ class BrowserScheduler {
const busy = [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name !== "ofdgoSaveSnapshot"); const busy = [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name !== "ofdgoSaveSnapshot");
const snapshot = task.data.name === "ofdgoSaveSnapshot" && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name === "ofdgoSaveSnapshot"); const snapshot = task.data.name === "ofdgoSaveSnapshot" && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID && data.name === "ofdgoSaveSnapshot");
const active = this.active(); const active = this.active();
const interactive = task.worker.primary && this.owner(task.worker, task.data)?.foreground === true && !task.data.preload; const interactive = task.worker.primary && this.owner(task.worker, task.data)?.foreground === true && !task.data.preload && !task.data.background;
const foregroundActive = [...this.workers].some(worker => [...worker.operations.values()].some(data => data.name !== "ofdgoSaveSnapshot" && worker.primary && this.owner(worker, data)?.foreground === true && !data.preload)); const foregroundActive = [...this.workers].some(worker => [...worker.operations.values()].some(data => data.name !== "ofdgoSaveSnapshot" && worker.primary && this.owner(worker, data)?.foreground === true && !data.preload && !data.background));
const continuing = task.worker.primary && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID); const continuing = task.worker.primary && [...task.worker.operations.values()].some(data => data.engineID === task.data.engineID);
const available = continuing || active < this.limit || interactive && !foregroundActive && active < this.limit + 1; const available = continuing || active < this.limit || interactive && !foregroundActive && active < this.limit + 1;
if (busy || snapshot || !available || !task.worker.primary && active && this.memory + (64 << 20) > this.budget) { i++; continue; } if (busy || snapshot || !available || !task.worker.primary && active && this.memory + (64 << 20) > this.budget) { i++; continue; }
@@ -294,12 +300,15 @@ class BrowserEngine {
const data = event.data; const data = event.data;
if (data.memory) this.memory = data.memory; if (data.memory) this.memory = data.memory;
if (this.source) this.parallelism = engineScheduler.limit; if (this.source) this.parallelism = engineScheduler.limit;
if (data.id != null && !data.type) this.requests.delete(data.id); const rendered = data.id != null && !data.type && this.requests.delete(data.id);
this.onmessage?.(event); this.onmessage?.(event);
if (rendered && data.ok) this.dispatchPages();
}; };
this.primary.onerror = event => this.onerror?.(event); this.primary.onerror = event => this.onerror?.(event);
this.primary.onmessageerror = event => this.onmessageerror?.(event); this.primary.onmessageerror = event => this.onmessageerror?.(event);
this.requests = new Map(); this.requests = new Map();
this.fontRequests = new Map();
this.primaryPages = new Set();
this.slots = new Set(); this.slots = new Set();
this.sequence = 0; this.sequence = 0;
this.revision = 0; this.revision = 0;
@@ -316,8 +325,9 @@ class BrowserEngine {
configurePages(source) { configurePages(source) {
const current = this.pageSource; const current = this.pageSource;
const fonts = (source?.fonts || []).map(font => font?.data !== undefined ? {name:font.name, data:font.data} : font); const fonts = (source?.fonts || []).map(font => font?.data !== undefined ? {name:font.name, data:font.data} : font);
const sameNames = JSON.stringify(source?.fontNames || []) === JSON.stringify(current?.fontNames || []);
const sameFonts = current && fonts.length === current.fonts.length && fonts.every((font, index) => font === current.fonts[index] || font?.data !== undefined && font.name === current.fonts[index]?.name && font.data === current.fonts[index]?.data); const sameFonts = current && fonts.length === current.fonts.length && fonts.every((font, index) => font === current.fonts[index] || font?.data !== undefined && font.name === current.fonts[index]?.name && font.data === current.fonts[index]?.data);
if (source && current && source.file === current.file && (source.document || 0) === current.document && (source.version || "") === current.version && source.annotations === current.annotations && source.revision === current.revision && sameFonts) return; if (source && current && source.file === current.file && !!source.pdf === !!current.pdf && (source.document || 0) === current.document && (source.version || "") === current.version && source.annotations === current.annotations && source.revision === current.revision && sameFonts && sameNames) return;
this.invalidatePages(); this.invalidatePages();
if (!source || this.closed) return; if (!source || this.closed) return;
this.pageSource = {...source, fonts, document:source.document || 0, version:source.version || ""}; this.pageSource = {...source, fonts, document:source.document || 0, version:source.version || ""};
@@ -340,21 +350,26 @@ class BrowserEngine {
invalidatePages() { invalidatePages() {
this.revision++; this.revision++;
this.primaryPages.clear();
this.source = null; this.source = null;
this.pageSource = null; this.pageSource = null;
this.parallelism = 1; this.parallelism = 1;
this.startupTime = 0;
this.primary.pageTime = 0;
for (const slot of this.slots) this.closeSlot(slot); for (const slot of this.slots) this.closeSlot(slot);
} }
closeSlot(slot, error = new DOMException("页面加载已取消", "AbortError")) { closeSlot(slot, error = new DOMException("页面加载已取消", "AbortError")) {
if (!this.slots.delete(slot)) return; if (!this.slots.delete(slot)) return;
slot.worker.terminate(); slot.worker.terminate();
for (const [id, request] of this.fontRequests) if (request.worker === slot.worker) this.fontRequests.delete(id);
slot.rejectReady(error); slot.rejectReady(error);
for (const pending of slot.pending.values()) pending.reject(error); for (const pending of slot.pending.values()) pending.reject(error);
slot.pending.clear(); slot.pending.clear();
} }
createSlot(source) { createSlot(source) {
const started = performance.now();
const worker = createEngineWorker({page:true, url:this.workerURL}); const worker = createEngineWorker({page:true, url:this.workerURL});
worker.setForeground(this.foreground); worker.setForeground(this.foreground);
worker.pausePages(this.pagesPaused); worker.pausePages(this.pagesPaused);
@@ -368,6 +383,12 @@ class BrowserEngine {
if (this.source) this.parallelism = engineScheduler.limit; if (this.source) this.parallelism = engineScheduler.limit;
if (data.type === "ready") { resolve(); return; } if (data.type === "ready") { resolve(); return; }
if (data.type === "exit") { this.closeSlot(slot, new Error("页面加载失败")); return; } if (data.type === "exit") { this.closeSlot(slot, new Error("页面加载失败")); return; }
if (data.type === "font") {
const request = `${slot.worker.engineID}:${data.request}`;
this.fontRequests.set(request, {worker:slot.worker, request:data.request});
this.onmessage?.({data:{...data, id:slot.requestID, request}});
return;
}
if (data.type) return; if (data.type) return;
const request = slot.pending.get(data.id); const request = slot.pending.get(data.id);
if (!request) return; if (!request) return;
@@ -379,9 +400,12 @@ class BrowserEngine {
}); });
this.slots.add(slot); this.slots.add(slot);
slot.ready = ready.then(async () => { slot.ready = ready.then(async () => {
let opened = await this.callSlot(slot, "ofdgoOpen", [source.file, source.fonts, source.annotations]); let opened = source.pdf
? await this.callSlot(slot, "ofdgoOpenPDF", [source.file, source.fonts, null, source.annotations, source.fontNames])
: 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.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 || ""]); if ((source.version || "") !== (opened.data.documentVersion || "")) await this.callSlot(slot, "ofdgoSelectVersion", [source.version || ""]);
if (this.slots.has(slot)) this.startupTime = performance.now() - started;
}); });
slot.ready.catch(() => {}); slot.ready.catch(() => {});
return slot; return slot;
@@ -397,20 +421,59 @@ class BrowserEngine {
}); });
} }
pageSlot(data) {
if (this.primaryPages.has(data.args[0])) return null;
const preferred = [...this.slots].find(slot => !slot.busy && slot.pages.has(data.args[0]));
const pending = [...this.requests.values()].filter(slot => slot === null).length - Number(this.requests.has(data.id));
if (!preferred && pending <= 0) return null;
let slot = preferred || [...this.slots].filter(slot => !slot.busy).sort((a,b) => Math.abs(a.lastPage-data.args[0])-Math.abs(b.lastPage-data.args[0]))[0];
const worthwhile = (this.primary.pageTime || 0) * pending >= Math.max(50, this.startupTime || this.primary.openTime || 0);
if (!slot && worthwhile && this.foreground && !this.pagesPaused && this.slots.size < this.parallelism - 1
&& engineScheduler.memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
return slot;
}
dispatchPages() {
if (this.closed || !this.source || !this.foreground || this.pagesPaused) return;
const draining = engineScheduler.draining;
engineScheduler.draining = true;
try {
const queued = engineScheduler.queue.filter(task => task.worker === this.primary.runtime && task.data.engineID === this.engineID).sort((a,b) => a.order-b.order);
for (const task of queued) {
if (task.data.name !== "ofdgoRenderPage") break;
if (this.requests.get(task.data.id) !== null) continue;
let slot;
try { slot = this.pageSlot(task.data); } catch { break; }
if (!slot) break;
engineScheduler.queue.splice(engineScheduler.queue.indexOf(task), 1);
this.primary.requests.delete(task.data.id);
this.renderSlot(slot, task.data, task.transfer);
}
} finally { engineScheduler.draining = draining; }
}
postMessage(data, transfer) { postMessage(data, transfer) {
if (this.closed) return; if (this.closed) return;
if (data.type === "font" && typeof data.request === "string") {
const request = this.fontRequests.get(data.request);
if (request) {
this.fontRequests.delete(data.request);
request.worker.postMessage({...data, request:request.request}, transfer);
}
return;
}
if (!data.type && data.preload && this.pagesPaused) { if (!data.type && data.preload && this.pagesPaused) {
this.onmessage?.({data:{engineID:this.engineID, id:data.id, ok:false, canceled:true, error:"预加载已暂停"}}); this.onmessage?.({data:{engineID:this.engineID, id:data.id, ok:false, canceled:true, error:"预加载已暂停"}});
return; return;
} }
if (data.type === "cancel") { if (data.type === "cancel") {
const slot = this.requests.get(data.id); const slot = this.requests.get(data.id);
if (slot) this.closeSlot(slot); if (slot && slot !== this.primary) this.closeSlot(slot);
else this.primary.postMessage(data); else this.primary.postMessage(data);
return; return;
} }
if (!data.type && data.name !== "ofdgoRenderPage" && ![ if (!data.type && data.name !== "ofdgoRenderPage" && ![
"ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData", "ofdgoLoadPDF", "ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData",
"ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets",
"ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles", "ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles",
"ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures", "ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures",
@@ -420,26 +483,30 @@ class BrowserEngine {
"ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoDiscardPrint", "ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoDiscardPrint",
"ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion" "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion"
].includes(data.name)) this.invalidatePages(); ].includes(data.name)) this.invalidatePages();
const preferred = data.name === "ofdgoRenderPage" ? [...this.slots].find(slot => !slot.busy && slot.pages.has(data.args[0])) : null; const slot = data.name === "ofdgoRenderPage" && this.source ? this.pageSlot(data) : null;
if (data.name !== "ofdgoRenderPage" || !this.source || !preferred && ![...this.requests.values()].includes(null)) { if (!slot) {
if (data.name === "ofdgoRenderPage") this.requests.set(data.id, null); if (data.name === "ofdgoRenderPage") this.requests.set(data.id, null);
this.primary.postMessage(data, transfer); this.primary.postMessage(data, transfer);
return; return;
} }
let slot = preferred || [...this.slots].filter(slot => !slot.busy).sort((a,b) => Math.abs(a.lastPage-data.args[0])-Math.abs(b.lastPage-data.args[0]))[0]; this.renderSlot(slot, data, transfer);
const memory = engineScheduler.memory; }
if (!slot && this.foreground && !this.pagesPaused && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
if (!slot) { renderSlot(slot, data, transfer) {
this.requests.set(data.id, null);
this.primary.postMessage(data, transfer);
return;
}
slot.busy = true; slot.busy = true;
slot.requestID = data.id;
this.requests.set(data.id, slot); this.requests.set(data.id, slot);
const revision = this.revision; const revision = this.revision;
let fallback = false; let fallback = false;
slot.ready.then(() => this.callSlot(slot, data.name, data.args, !!data.preload)).then(result => { slot.ready.then(() => this.callSlot(slot, data.name, data.args, !!data.preload)).then(result => {
if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError"); if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError");
if (this.source.pdf && ((JSON.parse(result.data.annotations || "null") || []).length || result.data.links?.length)) {
this.primaryPages.add(data.args[0]);
this.requests.set(data.id, this.primary);
fallback = true;
this.primary.postMessage(data, transfer);
return;
}
slot.lastPage = data.args[0]; slot.lastPage = data.args[0];
slot.pages.delete(slot.lastPage); slot.pages.delete(slot.lastPage);
slot.pages.add(slot.lastPage); slot.pages.add(slot.lastPage);
@@ -449,7 +516,7 @@ class BrowserEngine {
if (!this.closed && revision === this.revision && error.name !== "AbortError") { if (!this.closed && revision === this.revision && error.name !== "AbortError") {
this.closeSlot(slot, error); this.closeSlot(slot, error);
this.parallelism = Math.max(1, this.slots.size + 1); this.parallelism = Math.max(1, this.slots.size + 1);
this.requests.set(data.id, null); this.requests.set(data.id, this.primary);
fallback = true; fallback = true;
this.primary.postMessage(data, transfer); this.primary.postMessage(data, transfer);
return; return;
@@ -461,6 +528,7 @@ class BrowserEngine {
slot.worker.reservation = 0; slot.worker.reservation = 0;
const memory = engineScheduler.memory; const memory = engineScheduler.memory;
if (memory > this.budget) { this.closeSlot(slot); this.parallelism = Math.max(1, this.slots.size + 1); } if (memory > this.budget) { this.closeSlot(slot); this.parallelism = Math.max(1, this.slots.size + 1); }
this.dispatchPages();
engineScheduler.drain(); engineScheduler.drain();
}); });
} }
@@ -574,13 +642,13 @@ class NativeHost {
} }
} }
async decode(value, name = "", key = "", signal) { async decode(value, signal) {
const reads = []; const reads = [];
const visit = (item, field, replace) => { const visit = (item, field, replace) => {
if (!item || typeof item !== "object" || item instanceof Blob || item instanceof ArrayBuffer || ArrayBuffer.isView(item)) return; if (!item || typeof item !== "object" || item instanceof Blob || item instanceof ArrayBuffer || ArrayBuffer.isView(item)) return;
if (item.nativeFile) { if (item.nativeFile) {
const file = new NativeFile(item); const file = new NativeFile(item);
if (field === "file" || name === "ofdgoConvertPDF" && field === "bytes") replace(file); if (field === "file") replace(file);
else reads.push(this.readResource(file, signal).then(bytes => replace(field === "blob" ? new Blob([bytes], {type:item.type}) : new Uint8Array(bytes)))); else reads.push(this.readResource(file, signal).then(bytes => replace(field === "blob" ? new Blob([bytes], {type:item.type}) : new Uint8Array(bytes))));
return; return;
} }
@@ -588,7 +656,7 @@ class NativeHost {
if (content && typeof content === "object") visit(content, Array.isArray(item) ? "" : child, result => item[child] = result); if (content && typeof content === "object") visit(content, Array.isArray(item) ? "" : child, result => item[child] = result);
} }
}; };
visit(value, key, result => value = result); visit(value, "", result => value = result);
const results = await Promise.allSettled(reads); const results = await Promise.allSettled(reads);
const failed = results.find(result => result.status === "rejected"); const failed = results.find(result => result.status === "rejected");
if (failed) throw failed.reason; if (failed) throw failed.reason;
@@ -650,7 +718,7 @@ class NativeHost {
return; return;
} }
if (data.type === "font") return; if (data.type === "font") return;
if (["ofdgoDetectFormat", "ofdgoOpen", "ofdgoConvertPDF"].includes(data.name)) { if (["ofdgoDetectFormat", "ofdgoOpen", "ofdgoOpenPDF"].includes(data.name)) {
const input = data.args?.[0]; const input = data.args?.[0];
if (input?.nativeFile && !input.temporary) inputs.add(input.nativeFile); if (input?.nativeFile && !input.temporary) inputs.add(input.nativeFile);
} }
@@ -683,7 +751,7 @@ class NativeHost {
let decoded; let decoded;
try { try {
controller.signal.throwIfAborted(); controller.signal.throwIfAborted();
decoded = await host.decode(result, data.name, "", controller.signal); decoded = await host.decode(result, controller.signal);
controller.signal.throwIfAborted(); controller.signal.throwIfAborted();
} }
catch (error) { await host.release(result); throw error; } catch (error) { await host.release(result); throw error; }
+47 -31
View File
@@ -229,7 +229,7 @@ export function createPageView(context) {
for (const task of context.state.pagePresentQueue.values()) task.resolve(null); for (const task of context.state.pagePresentQueue.values()) task.resolve(null);
context.state.pagePresentQueue.clear(); context.state.pagePresentQueue.clear();
for (const [id, request] of context.wasmRequests) { for (const [id, request] of context.wasmRequests) {
if (request.tab === context.documentTab() && request.name === "ofdgoRenderPage") context.wasmWorker?.postMessage({ type: "cancel", id }); if (request.tab === context.documentTab() && request.name === "ofdgoRenderPage" && (!context.state.sourcePDF || context.state.pdfComplete || context.state.editorInfo)) context.wasmWorker?.postMessage({ type: "cancel", id });
} }
for (const task of context.state.pageInFlight.values()) { for (const task of context.state.pageInFlight.values()) {
task.resolve(null); task.resolve(null);
@@ -529,33 +529,37 @@ export function createPageView(context) {
}, },
async loadSVGFonts(fonts, openSeq) { async loadSVGFonts(fonts, openSeq) {
const state = context.state; const state = context.state, loadSeq = state.pageLoadSeq;
for (const font of fonts) { const current = () => state === context.state && openSeq === state.openSeq && loadSeq === state.pageLoadSeq;
if (openSeq !== context.state.openSeq) { for (let offset = 0; offset < fonts.length && current(); offset += 4) {
return; const results = await Promise.allSettled(fonts.slice(offset, offset + 4).map(async font => {
} if (state.svgFonts.has(font.name)) return;
if (state.svgFonts.has(font.name)) { const key = `${openSeq}:${loadSeq}:${font.name}`;
continue; while (current() && !state.svgFonts.has(font.name) && !state.svgFontLoads.has(key) && state.svgFontLoads.size >= 4) {
} await Promise.race([...state.svgFontLoads.values()].map(loading => loading.catch(() => {})));
const key = `${openSeq}:${font.name}`; }
let loading = state.svgFontLoads.get(key); if (!current() || state.svgFonts.has(font.name)) return;
if (!loading) { let loading = state.svgFontLoads.get(key);
loading = pageView.loadSVGFont(font, openSeq); if (!loading) {
state.svgFontLoads.set(key, loading); loading = pageView.loadSVGFont(font, openSeq, loadSeq);
} state.svgFontLoads.set(key, loading);
try { await loading; } }
finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); } try { await loading; }
finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); }
}));
const failed = results.find(result => result.status === "rejected");
if (failed) throw failed.reason;
} }
}, },
async loadSVGFont(font, openSeq) { async loadSVGFont(font, openSeq, loadSeq = context.state.pageLoadSeq) {
const data = font.data ? { bytes: font.data } : await context.callWASM("ofdgoSVGFontData", font.name); const data = font.data ? { bytes: font.data } : await context.callWASM("ofdgoSVGFontData", font.name);
if (openSeq !== context.state.openSeq) { if (openSeq !== context.state.openSeq || loadSeq !== context.state.pageLoadSeq) {
return; return;
} }
const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style }); const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style });
await face.load(); await face.load();
if (openSeq !== context.state.openSeq) { if (openSeq !== context.state.openSeq || loadSeq !== context.state.pageLoadSeq) {
return; return;
} }
document.fonts.add(face); document.fonts.add(face);
@@ -610,23 +614,27 @@ export function createPageView(context) {
const started = performance.now(); const started = performance.now();
do { do {
const task = [...state.pagePresentQueue.values()].sort(pageView.comparePageRenderTask)[0]; const task = [...state.pagePresentQueue.values()].sort(pageView.comparePageRenderTask)[0];
state.pagePresentQueue.delete(task.index);
try { try {
if (state.pageCache.get(task.index) === task.page) { if (state.pageCache.get(task.index) === task.page) {
if (task.shell && pageView.pageShell(task.index) === task.shell && !task.shell.classList.contains("rendered") if (task.shell && pageView.pageShell(task.index) === task.shell && !task.shell.classList.contains("rendered")
&& (!state.pageObserver || state.visiblePages.has(task.index) || task.index === state.pageIndex)) { && (!state.pageObserver || state.visiblePages.has(task.index) || task.index === state.pageIndex)) {
pageView.mountPageSVG(task.index, task.page, task.openSeq); task.text ||= pageView.createTextLayerParts(task.page.text);
let text;
do { text = task.text.next(); } while (!text.done && performance.now() - started < 6);
if (!text.done) break;
pageView.mountPageSVG(task.index, task.page, task.openSeq, text.value);
} }
if (task.thumb && context.el.pageList.querySelector(`[data-page-index="${task.index}"] .thumb-paper`) === task.thumb) pageView.updateThumbnail(task.index, task.openSeq); if (task.thumb && context.el.pageList.querySelector(`[data-page-index="${task.index}"] .thumb-paper`) === task.thumb) pageView.updateThumbnail(task.index, task.openSeq);
} }
task.resolve(null); task.resolve(null);
} catch (err) { task.reject(err); } } catch (err) { task.reject(err); }
state.pagePresentQueue.delete(task.index);
} while (state.pagePresentQueue.size && performance.now() - started < 6); } while (state.pagePresentQueue.size && performance.now() - started < 6);
pageView.schedulePagePresentation(); pageView.schedulePagePresentation();
}); });
}, },
mountPageSVG(index, page, openSeq = context.state.openSeq) { mountPageSVG(index, page, openSeq = context.state.openSeq, textLayer = null) {
if (openSeq !== context.state.openSeq) { if (openSeq !== context.state.openSeq) {
return; return;
} }
@@ -638,7 +646,7 @@ export function createPageView(context) {
const svg = pageView.parseSVG(page.svg, `p${openSeq}-${index}`); const svg = pageView.parseSVG(page.svg, `p${openSeq}-${index}`);
svg.classList.add("ofd-svg"); svg.classList.add("ofd-svg");
surface.replaceChildren(svg); surface.replaceChildren(svg);
surface.append(pageView.createTextLayer(page.text)); surface.append(textLayer || pageView.createTextLayer(page.text));
if (context.state.editorInfo && pageView.displayMode() === "svg") { if (context.state.editorInfo && pageView.displayMode() === "svg") {
context.mountEditorObjects(index, page, surface); context.mountEditorObjects(index, page, surface);
} }
@@ -1621,12 +1629,11 @@ export function createPageView(context) {
return next; return next;
}; };
const attrs = ["clip-path", "fill", "filter", "href", "marker-end", "marker-mid", "marker-start", "mask", "stroke", "style", "xlink:href"]; const attrs = ["clip-path", "fill", "filter", "href", "marker-end", "marker-mid", "marker-start", "mask", "stroke", "style", "xlink:href"];
for (const attr of attrs) { const selector = attrs.map(attr => `[${attr === "xlink:href" ? "*|href" : attr}*="#"]`).join(",");
for (const node of svg.querySelectorAll(`[${attr === "xlink:href" ? "*|href" : attr}*="#"]`)) { for (const node of svg.querySelectorAll(selector)) {
for (const attr of attrs) {
const value = node.getAttribute(attr); const value = node.getAttribute(attr);
if (!value) { if (!value?.includes("#")) continue;
continue;
}
const next = replaceRef(value); const next = replaceRef(value);
if (next !== value) { if (next !== value) {
node.setAttribute(attr, next); node.setAttribute(attr, next);
@@ -1636,11 +1643,19 @@ export function createPageView(context) {
}, },
createTextLayer(text) { createTextLayer(text) {
const parts = pageView.createTextLayerParts(text);
let next;
do { next = parts.next(); } while (!next.done);
return next.value;
},
*createTextLayerParts(text) {
const layer = document.createElement("div"); const layer = document.createElement("div");
layer.className = "text-layer"; layer.className = "text-layer";
const measure = textMeasure ||= document.createElement("canvas").getContext("2d"); const measure = textMeasure ||= document.createElement("canvas").getContext("2d");
measure.font = "100px sans-serif"; measure.font = "100px sans-serif";
const widths = new Map(); const widths = new Map();
let count = 0;
for (const run of text.runs || []) { for (const run of text.runs || []) {
const line = document.createElement("span"); const line = document.createElement("span");
line.className = "text-run"; line.className = "text-run";
@@ -1658,10 +1673,11 @@ export function createPageView(context) {
width = measure.measureText(span.textContent).width || 1; width = measure.measureText(span.textContent).width || 1;
widths.set(span.textContent, width); widths.set(span.textContent, width);
} }
const [a, b, c, d, e, f] = item.matrix.map((value) => value * MM_TO_PX); const [a, b, c, d, e, f] = item.matrix;
span.style.transform = `matrix(${a / width}, ${b / width}, ${c / 100}, ${d / 100}, ${e}, ${f})`; span.style.transform = `matrix(${a * MM_TO_PX / width}, ${b * MM_TO_PX / width}, ${c * MM_TO_PX / 100}, ${d * MM_TO_PX / 100}, ${e * MM_TO_PX}, ${f * MM_TO_PX})`;
line.append(span); line.append(span);
end = item.end; end = item.end;
if (++count % 128 === 0) yield;
} }
const breaks = chars.slice(end).filter((char) => char === "\n").join(""); const breaks = chars.slice(end).filter((char) => char === "\n").join("");
if (breaks) { if (breaks) {
+16 -3
View File
@@ -235,9 +235,9 @@ async function handleMessage(engine, { id, name, args }) {
let saveStamp; let saveStamp;
let channel; let channel;
const signal = operations.get(id)?.signal; const signal = operations.get(id)?.signal;
const cancellable = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot", "ofdgoImportDocuments", "ofdgoImportImages", "ofdgoExportImages", "ofdgoConvertPDF", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned", "ofdgoImportPages"].includes(name); const cancellable = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave", "ofdgoSaveSnapshot", "ofdgoImportDocuments", "ofdgoImportImages", "ofdgoExportImages", "ofdgoOpenPDF", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned", "ofdgoImportPages"].includes(name);
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments"; const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages" || name === "ofdgoImportDocuments";
const converting = name === "ofdgoConvertPDF"; const converting = name === "ofdgoOpenPDF";
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned"; const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
const resources = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave"].includes(name); const resources = ["ofdgoPreparePrint", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF", "ofdgoResources", "ofdgoPrepareSave"].includes(name);
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null; const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
@@ -245,7 +245,7 @@ async function handleMessage(engine, { id, name, args }) {
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null; const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
try { try {
signal?.throwIfAborted(); signal?.throwIfAborted();
if (["ofdgoOpen", "ofdgoLoadImport", "ofdgoConvertPDF"].includes(name) && args[0] instanceof Blob) { if (["ofdgoOpen", "ofdgoOpenPDF", "ofdgoLoadImport"].includes(name) && args[0] instanceof Blob) {
const file = args[0], reader = new FileReaderSync(); const file = args[0], reader = new FileReaderSync();
args[0] = {size: file.size, read: (offset, size) => new Uint8Array(reader.readAsArrayBuffer(file.slice(offset, offset + size)))}; args[0] = {size: file.size, read: (offset, size) => new Uint8Array(reader.readAsArrayBuffer(file.slice(offset, offset + size)))};
} }
@@ -337,9 +337,22 @@ async function handleMessage(engine, { id, name, args }) {
if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done)); if (!saving && args.some(value => value?.mode !== undefined)) args.push(done => finish(done));
} }
} }
if (engine.pdf && name !== "ofdgoOpenPDF") {
channel ||= new MessageChannel();
const context = await globalThis.ofdgoEngineCall(engine.id, "ofdgoPDFContext", (phase, completed, total, done) => {
postMessage({id, type: "conversion", phase, completed, total});
channel.port1.onmessage = () => done("", signal.aborted);
channel.port2.postMessage(null);
}, (name, done) => requestFont(engine, id, signal, name, done));
engine.pdf = JSON.parse(context).data === true;
}
const started = name === "ofdgoOpen" || name === "ofdgoRenderPage" ? performance.now() : null;
const payload = await globalThis.ofdgoEngineCall(engine.id, name, ...args); const payload = await globalThis.ofdgoEngineCall(engine.id, name, ...args);
signal?.throwIfAborted(); signal?.throwIfAborted();
const result = typeof payload === "string" ? JSON.parse(payload) : payload; const result = typeof payload === "string" ? JSON.parse(payload) : payload;
if (result.ok && name === "ofdgoOpenPDF") engine.pdf = true;
if (result.ok && (name === "ofdgoOpen" || name === "ofdgoLoadPDF" && result.data.done)) engine.pdf = false;
if (started !== null && result.ok) result.elapsed = performance.now() - started;
if (cancellable && !importing && !converting && !resources && result.ok) { if (cancellable && !importing && !converting && !resources && result.ok) {
result.data.size = size; result.data.size = size;
if (name !== "ofdgoExportMedia") postMessage({ id, type: "export", stage: "save" }); if (name !== "ofdgoExportMedia") postMessage({ id, type: "export", stage: "save" });
+245 -39
View File
@@ -32,6 +32,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xiaoqidun/ofdgo" "github.com/xiaoqidun/ofdgo"
@@ -435,15 +436,30 @@ type outputContext struct {
cancel context.CancelFunc cancel context.CancelFunc
checkpoint func() error checkpoint func() error
checked time.Time checked time.Time
pending uint8 checks atomic.Uint32
interval uint8 interval uint8
} }
// pendingPDF 保存当前标签的增量转换、输入及操作回调
type pendingPDF struct {
importer *ofdgo.PDFImport
retry bool
input *ofdgo.Input
page int
next int
progress platformValue
font platformValue
operation *outputContext
report func(string, int, int) error
}
// businessEngine 独立文档、编辑、剪贴板及操作上下文 // businessEngine 独立文档、编辑、剪贴板及操作上下文
type businessEngine struct { type businessEngine struct {
resources *ofdgo.Resources resources *ofdgo.Resources
currentSession *Session currentSession *Session
currentEditor *ofdgo.Editor currentEditor *ofdgo.Editor
pdf *pendingPDF
pdfWarnings []byte
saveSnapshot *ofdgo.Editor saveSnapshot *ofdgo.Editor
printDocument *ofdgo.PrintDocument printDocument *ofdgo.PrintDocument
pendingImport *ofdgo.Reader pendingImport *ofdgo.Reader
@@ -1394,58 +1410,243 @@ func (e *businessEngine) detectDocumentFormat(args []platformValue) (any, error)
return ofdgo.DetectFormat(input) return ofdgo.DetectFormat(input)
} }
// convertPDFDocument 通过库转换PDF,不替换当前文档会话 // clearPDF 释放当前标签的未完成转换和输入
// 入参: args PDF数据、外部字体、UTF-8密码、字体目录、进度及字体读取回调 func (e *businessEngine) clearPDF() {
// 返回: any OFD数据及转换警告, error 错误信息 e.pdfWarnings = nil
func (e *businessEngine) convertPDFDocument(args []platformValue) (any, error) { if e.pdf == nil {
if len(args) != 6 { return
return nil, fmt.Errorf("missing PDF data") }
e.pdf.importer.Close()
e.pdf.input.Close()
e.pdf = nil
}
// pdfContext 更新当前操作的取消、进度和按需字体读取回调
// 入参: args 进度及字体回调
// 返回: any 是否仍有待转换内容, error 参数错误
func (e *businessEngine) pdfContext(args []platformValue) (any, error) {
if len(args) != 2 {
return nil, fmt.Errorf("missing PDF callbacks")
}
if e.pdf != nil {
e.pdf.progress, e.pdf.font = args[0], args[1]
}
return e.pdf != nil, nil
}
// openPDF 建立PDF索引,当前页转换由阅读请求触发
// 入参: args 输入、字体、密码、注解设置、字体目录、进度及字体回调
// 返回: any 文档索引, error 打开错误
func (e *businessEngine) openPDF(args []platformValue) (any, error) {
if len(args) != 7 {
return nil, fmt.Errorf("missing PDF input")
} }
input, err := e.inputFromValue(args[0]) input, err := e.inputFromValue(args[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer input.Close() pending := &pendingPDF{input: input, page: -1, progress: args[5], font: args[6]}
keep := false
var password []byte
defer func() { clear(password) }()
defer func() {
if !keep {
input.Close()
if pending.importer != nil {
pending.importer.Close()
}
}
}()
fonts, err := e.fontsFromJS(args[1]) fonts, err := e.fontsFromJS(args[1])
if err != nil { if err != nil {
return nil, err return nil, err
} }
var output bytes.Buffer
options := ofdgo.PDFImportOptions{Resources: e.resources, OnProgress: e.editorOperationProgress(args[4]), PasswordUTF8: true}
if !args[2].IsNull() && !args[2].IsUndefined() { if !args[2].IsNull() && !args[2].IsUndefined() {
options.Password, err = e.bytesFromJS(args[2]) password, err = e.bytesFromJS(args[2])
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer clear(options.Password) password = slices.Clone(password)
} }
fontSources := []fs.FS{ofdgo.NewFontFS(fonts)} pending.operation, pending.report = e.newEditorOperationContext(pending.progress)
if names := e.stringsFromJS(args[3]); len(names) > 0 { options := ofdgo.PDFImportOptions{Resources: e.resources, PasswordUTF8: true, Password: password,
fontSources = append(fontSources, ofdgo.NewLazyFontFS(names, func(name string) ([]byte, error) { OnProgress: func(stage string, completed, total int) error {
return e.loadConversionFont(args[5], name) return pending.report(stage, completed, total)
},
}
sources := []fs.FS{ofdgo.NewFontFS(fonts)}
if names := e.stringsFromJS(args[4]); len(names) != 0 {
sources = append(sources, ofdgo.NewLazyFontFS(names, func(name string) ([]byte, error) {
return e.loadConversionFont(pending.font, name)
})) }))
} }
options.RendererOptions = []ofdgo.RendererOption{ofdgo.WithFontFS(fontSources...)} options.RendererOptions = []ofdgo.RendererOption{ofdgo.WithFontFS(sources...)}
editor, report, err := ofdgo.ImportPDF(e.operationContext(), input, options) pending.importer, err = ofdgo.NewPDFImport(pending.operation, input, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(options.Password) != 0 { if len(password) != 0 {
if err := editor.SetEncryption(&ofdgo.EncryptionOptions{Password: options.Password}); err != nil { if err := pending.importer.SetEncryption(&ofdgo.EncryptionOptions{Password: password}); err != nil {
return nil, err return nil, err
} }
} }
editor.OnWriteProgress = func(stage string, completed, total int) error { reader, err := pending.importer.Reader(pending.operation, -1)
return options.OnProgress("write."+stage, completed, total)
}
if _, err := editor.WriteTo(&output); err != nil {
return nil, err
}
warnings, err := json.Marshal(report.Warnings)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return successResult(map[string]any{"bytes": bytesToJS(output.Bytes()), "warnings": string(warnings)}), nil session, err := newSession(reader, OpenOptions{Fonts: fonts, RenderAnnotations: args[3].Bool()})
if err != nil {
return nil, err
}
e.clearPDF()
e.clearImport()
e.currentSession.Close()
e.currentSession, e.currentEditor = session, nil
e.copiedObjects, e.copiedStyle = nil, nil
e.pdf = pending
keep = true
return session.Summary(), nil
}
// pdfError 记录需要重建的取消状态,后续请求可继续使用原文档
// 入参: err 转换错误
// 返回: error 原错误
func (e *businessEngine) pdfError(err error) error {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
e.pdf.retry = true
}
return err
}
// replacePDFReader 切换独立阅读快照,保留当前字体和注解设置
// 入参: reader 已生成的阅读器
// 返回: error 会话创建错误
func (e *businessEngine) replacePDFReader(reader *ofdgo.Reader) error {
previous := e.currentSession
next, err := newSession(reader, OpenOptions{RenderAnnotations: previous.Renderer.RenderAnnotations})
if err != nil {
return err
}
dirs, sources := previous.Renderer.FontSources()
ofdgo.WithFontDirs(dirs...)(next.Renderer)
ofdgo.WithFontFS(sources...)(next.Renderer)
ofdgo.WithRenderBackends(previous.Renderer.Backends())(next.Renderer)
next.Renderer.OutputContext = e.pdf.operation
next.fontFS = previous.fontFS
next.svgFonts = previous.svgFonts
e.currentSession = next
return previous.Close()
}
// finishPDF 补全转换后直接创建阅读快照,不生成中间压缩包
// 返回: []byte 转换警告JSON, error 转换错误
func (e *businessEngine) finishPDF() ([]byte, error) {
if e.pdf == nil {
return e.pdfWarnings, nil
}
editor, report, err := e.pdf.importer.Finish(e.pdf.operation)
if err != nil {
return nil, e.pdfError(err)
}
reader, err := editor.Reader(e.pdf.operation)
if err != nil {
return nil, err
}
if err := e.replacePDFReader(reader); err != nil {
return nil, err
}
warnings, err := json.Marshal(report.Warnings)
e.clearPDF()
e.pdfWarnings = warnings
return warnings, err
}
// loadPDF 按时间片补全未转换页面,全部完成后交付完整会话
// 入参: args 空参数
// 返回: any 完成标识、文档信息及警告, error 转换错误
func (e *businessEngine) loadPDF(args []platformValue) (any, error) {
if e.currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
if e.pdf != nil {
deadline := time.Now().Add(32 * time.Millisecond)
for e.pdf.next < e.pdf.importer.PageCount() {
index := e.pdf.next
if !e.pdf.importer.Ready(index) {
if err := e.pdf.importer.ConvertPage(e.pdf.operation, index); err != nil {
return nil, e.pdfError(err)
}
}
e.pdf.next++
if time.Now().After(deadline) {
return map[string]any{"done": false}, nil
}
}
}
warnings, err := e.finishPDF()
if err != nil {
return nil, err
}
return map[string]any{"done": true, "doc": e.currentSession.Summary(), "warnings": string(warnings)}, nil
}
// preparePDF 按命令准备页面快照或完整文档,不将未完成内容交给编辑和输出
// 入参: name 命令名称, args 命令参数
// 返回: error 转换或取消错误
func (e *businessEngine) preparePDF(name string, args []platformValue) error {
if e.pdf == nil {
return nil
}
if name == "ofdgoPDFContext" || name == "ofdgoOpenPDF" || name == "ofdgoOpen" {
return nil
}
e.pdf.operation, e.pdf.report = e.newEditorOperationContext(e.pdf.progress)
e.pdf.operation.checked = time.Now()
e.pdf.operation.interval = 255
e.currentSession.Renderer.OutputContext = e.pdf.operation
if e.pdf.retry {
if err := e.pdf.importer.Reset(e.pdf.operation); err != nil {
return err
}
e.pdf.page, e.pdf.next, e.pdf.retry = -1, 0, false
reader, err := e.pdf.importer.Reader(e.pdf.operation, -1)
if err != nil {
return err
}
if err := e.replacePDFReader(reader); err != nil {
return err
}
}
switch name {
case "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices":
return nil
case "ofdgoOpenLinks":
if len(args) == 0 {
return nil
}
fallthrough
case "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString":
if len(args) == 0 {
return fmt.Errorf("missing page index")
}
index := args[0].Int()
if e.pdf.page == index {
return nil
}
reader, err := e.pdf.importer.Reader(e.pdf.operation, index)
if err != nil {
return e.pdfError(err)
}
if err := e.replacePDFReader(reader); err != nil {
return err
}
e.pdf.page = index
return nil
default:
_, err := e.finishPDF()
return err
}
} }
// convertFile 使用库转换独立文件,不读取或修改当前阅读和编辑会话 // convertFile 使用库转换独立文件,不读取或修改当前阅读和编辑会话
@@ -1641,6 +1842,7 @@ func (e *businessEngine) openDocument(args []platformValue) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
e.clearPDF()
e.clearImport() e.clearImport()
e.currentEditor = nil e.currentEditor = nil
e.copiedObjects = nil e.copiedObjects = nil
@@ -1689,6 +1891,9 @@ func (e *businessEngine) configureDocument(args []platformValue) (any, error) {
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息 // 返回: any 文档信息, error 错误信息
func (e *businessEngine) documentInfo(args []platformValue) (any, error) { func (e *businessEngine) documentInfo(args []platformValue) (any, error) {
if e.pdf != nil {
return map[string]any{"detailsPending": true}, nil
}
if e.currentSession == nil { if e.currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened") return nil, fmt.Errorf("ofd document is not opened")
} }
@@ -7418,8 +7623,8 @@ func (e *businessEngine) newOutputContext(checkpoint platformValue) *outputConte
// newEditorOperationContext 在底层取消检查时保持最近的进度,避免版本处理期间界面无响应 // newEditorOperationContext 在底层取消检查时保持最近的进度,避免版本处理期间界面无响应
// 入参: callback 浏览器进度及取消回调 // 入参: callback 浏览器进度及取消回调
// 返回: context.Context 操作上下文, func(string, int, int) error 阶段进度回调 // 返回: *outputContext 操作上下文, func(string, int, int) error 阶段进度回调
func (e *businessEngine) newEditorOperationContext(callback platformValue) (context.Context, func(string, int, int) error) { func (e *businessEngine) newEditorOperationContext(callback platformValue) (*outputContext, func(string, int, int) error) {
ctx, cancel := context.WithCancel(e.operationContext()) ctx, cancel := context.WithCancel(e.operationContext())
stage, completed, total := "ids", 0, 0 stage, completed, total := "ids", 0, 0
progress := e.editorOperationProgress(callback) progress := e.editorOperationProgress(callback)
@@ -7436,16 +7641,15 @@ func (c *outputContext) Err() error {
if err := c.Context.Err(); err != nil { if err := c.Context.Err(); err != nil {
return err return err
} }
c.mu.Lock() interval := uint32(c.interval) + 1
defer c.mu.Unlock() if c.interval == 0 {
if c.pending != 0 { interval = 16
c.pending-- }
if (c.checks.Add(1)-1)%interval != 0 {
return nil return nil
} }
c.pending = 15 c.mu.Lock()
if c.interval != 0 { defer c.mu.Unlock()
c.pending = c.interval
}
if time.Since(c.checked) >= 50*time.Millisecond { if time.Since(c.checked) >= 50*time.Millisecond {
c.checked = time.Now() c.checked = time.Now()
if err := c.checkpoint(); err != nil { if err := c.checkpoint(); err != nil {
@@ -7465,6 +7669,9 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoDiscardPrint": e.discardPrint, "ofdgoDiscardPrint": e.discardPrint,
"ofdgoDetectFormat": e.detectDocumentFormat, "ofdgoDetectFormat": e.detectDocumentFormat,
"ofdgoOpen": e.openDocument, "ofdgoOpen": e.openDocument,
"ofdgoOpenPDF": e.openPDF,
"ofdgoLoadPDF": e.loadPDF,
"ofdgoPDFContext": e.pdfContext,
"ofdgoSelectDocument": e.selectDocument, "ofdgoSelectDocument": e.selectDocument,
"ofdgoDocumentVersions": e.documentVersions, "ofdgoDocumentVersions": e.documentVersions,
"ofdgoExportVersion": e.exportVersion, "ofdgoExportVersion": e.exportVersion,
@@ -7488,7 +7695,6 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoChangeVersion": e.changeDocumentVersion, "ofdgoChangeVersion": e.changeDocumentVersion,
"ofdgoChangeDocument": e.changePackageDocument, "ofdgoChangeDocument": e.changePackageDocument,
"ofdgoPackageDocuments": e.packageDocuments, "ofdgoPackageDocuments": e.packageDocuments,
"ofdgoConvertPDF": e.convertPDFDocument,
"ofdgoConvertFile": e.convertFile, "ofdgoConvertFile": e.convertFile,
"ofdgoPackFiles": e.packFiles, "ofdgoPackFiles": e.packFiles,
"ofdgoOutputFormats": func([]platformValue) (any, error) { return ofdgo.OutputFormats(), nil }, "ofdgoOutputFormats": func([]platformValue) (any, error) { return ofdgo.OutputFormats(), nil },
+10 -2
View File
@@ -191,7 +191,7 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
if e.closed { if e.closed {
return nativeFailure(fmt.Errorf("native engine is closed")) return nativeFailure(fmt.Errorf("native engine is closed"))
} }
if name == "ofdgoRenderPage" && e.currentSession != nil { if name == "ofdgoRenderPage" && e.currentSession != nil && e.pdf == nil {
defer func() { defer func() {
if failure := recover(); failure != nil { if failure := recover(); failure != nil {
result = nativeFailure(fmt.Errorf("%v", failure)) result = nativeFailure(fmt.Errorf("%v", failure))
@@ -312,6 +312,10 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
return nativeFailure(fmt.Errorf("unknown operation: %s", name)) return nativeFailure(fmt.Errorf("unknown operation: %s", name))
} }
call := &nativeCall{engine: e, ctx: ctx, emit: emit} call := &nativeCall{engine: e, ctx: ctx, emit: emit}
if e.pdf != nil {
e.pdf.progress = platformValueOf(call.progress("conversion", "", true))
e.pdf.font = platformValueOf(nativeCallback(call.font))
}
defer func() { defer func() {
if err := call.abort(); err != nil { if err := call.abort(); err != nil {
if result == nil || result["ok"] == true { if result == nil || result["ok"] == true {
@@ -329,6 +333,9 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
for i, arg := range prepared { for i, arg := range prepared {
values[i] = platformValueOf(arg) values[i] = platformValueOf(arg)
} }
if err := e.preparePDF(name, values); err != nil {
return nativeFailure(err)
}
data, err := e.safeCall(fn, values) data, err := e.safeCall(fn, values)
if err != nil { if err != nil {
return nativeFailure(err) return nativeFailure(err)
@@ -357,6 +364,7 @@ func (e *NativeEngine) Close() error {
e.gate <- struct{}{} e.gate <- struct{}{}
defer func() { <-e.gate }() defer func() { <-e.gate }()
e.closed = true e.closed = true
e.clearPDF()
_, _ = e.discardPrint(nil) _, _ = e.discardPrint(nil)
_ = e.waitRenders(context.Background()) _ = e.waitRenders(context.Background())
e.renderIdle = nil e.renderIdle = nil
@@ -955,7 +963,7 @@ func (c *nativeCall) prepare(name string, args []any) ([]any, error) {
return append(args, checkpoint), nil return append(args, checkpoint), nil
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages": case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
return append(args, c.progress("import", "", true)), nil return append(args, c.progress("import", "", true)), nil
case "ofdgoConvertPDF": case "ofdgoOpenPDF":
return append(args, c.progress("conversion", "", true), nativeCallback(c.font)), nil return append(args, c.progress("conversion", "", true), nativeCallback(c.font)), nil
case "ofdgoExportImages", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned": case "ofdgoExportImages", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned":
if len(args) == 0 { if len(args) == 0 {
+11
View File
@@ -79,10 +79,20 @@ func (w *wasmEngines) call(args []js.Value) (any, error) {
if e == nil { if e == nil {
return nil, fmt.Errorf("unknown engine: %s", args[0].String()) return nil, fmt.Errorf("unknown engine: %s", args[0].String())
} }
if e.pdf != nil {
defer func() {
if e.currentSession != nil {
e.currentSession.Renderer.OutputContext = context.Background()
}
}()
}
fn := e.businessCommands()[args[1].String()] fn := e.businessCommands()[args[1].String()]
if fn == nil { if fn == nil {
return nil, fmt.Errorf("unknown operation: %s", args[1].String()) return nil, fmt.Errorf("unknown operation: %s", args[1].String())
} }
if err := e.preparePDF(args[1].String(), args[2:]); err != nil {
return nil, err
}
return fn(args[2:]) return fn(args[2:])
} }
@@ -100,6 +110,7 @@ func (w *wasmEngines) close(args []js.Value) (any, error) {
} }
delete(w.engines, id) delete(w.engines, id)
_, _ = e.discardPrint(nil) _, _ = e.discardPrint(nil)
e.clearPDF()
e.clearImport() e.clearImport()
err := e.currentSession.Close() err := e.currentSession.Close()
*e = businessEngine{} *e = businessEngine{}
+3
View File
@@ -131,6 +131,9 @@ func (r *Reader) Editor() (*Editor, error) {
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: *Editor 编辑器, error 初始化或取消错误 // 返回: *Editor 编辑器, error 初始化或取消错误
func (r *Reader) EditorContext(ctx context.Context) (*Editor, error) { func (r *Reader) EditorContext(ctx context.Context) (*Editor, error) {
if r.pageReady != nil {
return nil, ErrIncompleteDocument
}
_, err := r.DocContext(ctx) _, err := r.DocContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
+262 -68
View File
@@ -69,6 +69,25 @@ type PDFImportReport struct {
Warnings []pdfgo.Diagnostic Warnings []pdfgo.Diagnostic
} }
// PDFImport 按页转换PDF,实例需串行使用,输入源由调用方持有至Close
// 页面阅读快照独立于后续转换,Finish完成全局结构后交付独立编辑文档
// 转换失败后可用Reset重建,输入及回调在会话存续期间须保持有效
type PDFImport struct {
input *Input
importer *pdfImporter
options PDFImportOptions
pages []PageContent
ready []bool
err error
finished bool
}
// pdfImportInput 将输入读取绑定到当前转换上下文
type pdfImportInput struct {
input *Input
importer *pdfImporter
}
// pdfImporter 保存单次转换的对象与资源状态 // pdfImporter 保存单次转换的对象与资源状态
type pdfImporter struct { type pdfImporter struct {
resources *Resources resources *Resources
@@ -212,21 +231,34 @@ func (o pdfObjectOrigin) merge(other pdfObjectOrigin) pdfObjectOrigin {
// 入参: ctx 取消上下文, input 输入源, options 转换选项 // 入参: ctx 取消上下文, input 输入源, options 转换选项
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息 // 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Editor, PDFImportReport, error) { func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Editor, PDFImportReport, error) {
pending, err := NewPDFImport(ctx, input, options)
if err != nil {
return nil, PDFImportReport{}, err
}
defer pending.Close()
return pending.Finish(ctx)
}
// NewPDFImport 建立PDF页面索引和文档信息,不转换页面图元
// 内部复制输入密码供Reset使用,Close时清除;其他选项引用需保持有效
// 入参: ctx 取消上下文, input 调用方持有的输入源, options 转换选项
// 返回: *PDFImport 增量转换会话, error 读取错误
func NewPDFImport(ctx context.Context, input *Input, options PDFImportOptions) (*PDFImport, error) {
if options.Resources == nil { if options.Resources == nil {
options.Resources, _ = NewResources(ResourceOptions{}) options.Resources, _ = NewResources(ResourceOptions{})
} }
ctx = options.Resources.context(ctx) ctx = options.Resources.context(ctx)
if input == nil { if input == nil {
return nil, PDFImportReport{}, fmt.Errorf("missing source") return nil, fmt.Errorf("missing source")
} }
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
if err := options.Compression.Validate(); err != nil { if err := options.Compression.Validate(); err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
if !finite(options.RasterDPI) || options.RasterDPI < 0 { if !finite(options.RasterDPI) || options.RasterDPI < 0 {
return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI") return nil, fmt.Errorf("invalid PDF raster DPI")
} }
renderer := NewRenderer(&Reader{resources: options.Resources}, options.RendererOptions...) renderer := NewRenderer(&Reader{resources: options.Resources}, options.RendererOptions...)
if options.Backends != nil { if options.Backends != nil {
@@ -236,18 +268,25 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
renderer.DPI = options.RasterDPI renderer.DPI = options.RasterDPI
} }
if !finite(renderer.DPI) || renderer.DPI <= 0 { if !finite(renderer.DPI) || renderer.DPI <= 0 {
return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI") return nil, fmt.Errorf("invalid PDF raster DPI")
} }
keep := false
options.Password = slices.Clone(options.Password)
defer func() {
if !keep {
clear(options.Password)
}
}()
progress := editorProgress(options.OnProgress).withContext(ctx) progress := editorProgress(options.OnProgress).withContext(ctx)
if err := progress.report("open", 0, 0); err != nil { if err := progress.report("open", 0, 0); err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
editor := NewEditor() editor := NewEditor()
editor.runtimeResources = options.Resources editor.runtimeResources = options.Resources
editor.SetRenderBackends(renderer.Backends()) editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources() editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{resources: options.Resources, ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, page: -1, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}} importer := &pdfImporter{resources: options.Resources, ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, page: -1, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}}
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText} readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText}
if !options.Strict { if !options.Strict {
importer.warning = func(warning pdfgo.Diagnostic) { importer.warning = func(warning pdfgo.Diagnostic) {
@@ -256,22 +295,26 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
} }
readOptions.Warning = importer.warning readOptions.Warning = importer.warning
} }
reader, err := pdfgo.NewReaderWithOptions(convertReader{ctx: ctx, input: input}, input.Size(), readOptions) reader, err := pdfgo.NewReaderWithOptions(&pdfImportInput{input: input, importer: importer}, input.Size(), readOptions)
if err != nil { if err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
defer reader.Close() defer func() {
if !keep {
reader.Close()
}
}()
importer.reader = reader importer.reader = reader
if err := importer.prepareStructure(ctx); err != nil { if err := importer.prepareStructure(ctx); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF structure: %w", err) return nil, fmt.Errorf("import PDF structure: %w", err)
} }
outputIntents, err := reader.OutputIntents() outputIntents, err := reader.OutputIntents()
if err != nil { if err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF output intents: %w", err) return nil, fmt.Errorf("import PDF output intents: %w", err)
} }
if len(outputIntents) != 0 { if len(outputIntents) != 0 {
if options.Strict { if options.Strict {
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"} return nil, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"}
} }
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents preserved as metadata; not applied to rendering"}) importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents preserved as metadata; not applied to rendering"})
} }
@@ -281,10 +324,14 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
importer.halftones = options.Halftones importer.halftones = options.Halftones
importer.compression = options.Compression importer.compression = options.Compression
importer.references = &codecpdf.ReferenceCache{} importer.references = &codecpdf.ReferenceCache{}
defer importer.references.Close() defer func() {
if !keep {
importer.references.Close()
}
}()
importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool) importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
if err := progress.report("pages", 0, 0); err != nil { if err := progress.report("pages", 0, 0); err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error {
_, width, height := codecpdf.PageMatrix(page) _, width, height := codecpdf.PageMatrix(page)
@@ -298,82 +345,229 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
return progress.report("pages", index+1, 0) return progress.report("pages", index+1, 0)
}) })
if err != nil { if err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
if len(editor.pages) != 0 { if len(editor.pages) != 0 {
importer.page = -1 importer.page = -1
if err := importer.documentMetadata(ctx, options.Strict); err != nil { if err := importer.documentMetadata(ctx, options.Strict); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF metadata: %w", err) return nil, fmt.Errorf("import PDF metadata: %w", err)
} }
if err := importer.documentAttachments(ctx); err != nil { if err := importer.documentAttachments(ctx); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF attachments: %w", err) return nil, fmt.Errorf("import PDF attachments: %w", err)
} }
if err := importer.outlines(ctx); err != nil { if err := importer.outlines(ctx); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF outlines: %w", err) return nil, fmt.Errorf("import PDF outlines: %w", err)
} }
if err := importer.documentOpenActions(ctx); err != nil { if err := importer.documentOpenActions(ctx); err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF document actions: %w", err) return nil, fmt.Errorf("import PDF document actions: %w", err)
} }
importer.page = 0 importer.page = 0
} }
if err := progress.report("convert", 0, len(editor.pages)); err != nil { if err := progress.report("convert", 0, len(editor.pages)); err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
err = reader.WalkPages(ctx, func(index int, page *pdfgo.Page) error { if len(editor.pages) == 0 {
matrix, width, height := codecpdf.PageMatrix(page) return nil, fmt.Errorf("PDF contains no pages")
importer.matrix = matrix }
importer.page = index keep = true
importer.rasterWarned = false return &PDFImport{input: input, importer: importer, options: options, pages: cloneEditorData(editor.pages), ready: make([]bool, len(editor.pages))}, nil
importer.pageBox = page.CropBox }
importer.pageWidth = width
importer.pageHeight = height // ReadAt 读取当前操作需要的数据,不取得输入所有权
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path) // 入参: data 目标缓冲, offset 文件偏移
importer.compositeCache = nil // 返回: int 读取字节数, error 读取或取消错误
importer.compositeNodes = nil func (r *pdfImportInput) ReadAt(data []byte, offset int64) (int, error) {
importer.compositeSpace = nil if err := r.importer.ctx.Err(); err != nil {
importer.patternCells = nil return 0, err
importer.patternOrigins = nil }
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject) return r.input.ReadAt(data, offset)
importer.textOrigins = nil }
importer.clipPaths = nil
importer.annotationData = nil // PageCount 返回文档页数
importer.annotationBindings = nil // 返回: int 页面总数
if err := importer.pageOpenActions(ctx, page); err != nil { func (p *PDFImport) PageCount() int { return len(p.pages) }
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
// Ready 返回指定页是否转换完成
// 入参: index 页面索引,从0开始
// 返回: bool 页面是否就绪
func (p *PDFImport) Ready(index int) bool {
return index >= 0 && index < len(p.ready) && p.ready[index]
}
// SetEncryption 设置转换文档的输出加密方式,不改变PDF读取凭据
// 入参: options 加密选项,nil取消输出加密
// 返回: error 加密配置错误
func (p *PDFImport) SetEncryption(options *EncryptionOptions) error {
if p.importer == nil {
return fmt.Errorf("PDF import is closed")
}
return p.importer.editor.SetEncryption(options)
}
// ConvertPage 转换指定页面,已完成的页面不重复处理
// 入参: ctx 取消上下文, index 页面索引,从0开始
// 返回: error 转换或取消错误
func (p *PDFImport) ConvertPage(ctx context.Context, index int) (err error) {
if p.importer == nil {
return fmt.Errorf("PDF import is closed")
}
if p.err != nil {
return p.err
}
if index < 0 || index >= len(p.pages) {
return fmt.Errorf("invalid page index: %d", index)
}
if err := ctx.Err(); err != nil {
return err
}
if p.ready[index] {
return nil
}
defer func() {
if err != nil {
p.err = err
} }
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil { }()
return fmt.Errorf("import PDF page %d: %w", index+1, err) importer := p.importer
} importer.ctx = importer.resources.context(ctx)
if err := importer.annotations(ctx, page, options.Strict); err != nil { ctx = importer.ctx
return fmt.Errorf("import PDF page %d: %w", index+1, err) importer.renderer.OutputContext = ctx
} page := importer.pageOrder[index]
if err := importer.commitAnnotations(); err != nil { matrix, width, height := codecpdf.PageMatrix(page)
return fmt.Errorf("import PDF page %d: %w", index+1, err) importer.matrix = matrix
} importer.page = index
if err := importer.commitObjects(); err != nil { importer.rasterWarned = false
return fmt.Errorf("import PDF page %d: %w", index+1, err) importer.pageBox = page.CropBox
} importer.pageWidth = width
importer.report.Pages++ importer.pageHeight = height
if err := progress.report("convert", index+1, len(editor.pages)); err != nil { importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
importer.compositeCache = nil
importer.compositeNodes = nil
importer.compositeSpace = nil
importer.patternCells = nil
importer.patternOrigins = nil
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
importer.textOrigins = nil
importer.clipPaths = nil
importer.annotationData = nil
importer.annotationBindings = nil
if err := importer.pageOpenActions(ctx, page); err != nil {
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
}
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return importer.reader.WalkPage(ctx, page, visitor) }); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
if err := importer.annotations(ctx, page, p.options.Strict); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
if err := importer.commitAnnotations(); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
if err := importer.commitObjects(); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
importer.report.Pages++
if err := editorProgress(p.options.OnProgress).withContext(ctx).report("convert", importer.report.Pages, len(p.pages)); err != nil {
return err
}
if p.options.Progress != nil {
if err := p.options.Progress(importer.report.Pages); err != nil {
return err return err
} }
if options.Progress != nil { }
if err := options.Progress(index + 1); err != nil {
return err p.ready[index] = true
} return nil
}
// Reader 创建指定页的独立阅读快照,其他页仅保留尺寸和标识
// 未包含的页面返回ErrIncompleteDocument;index为-1时仅返回索引,完整编辑或导出须使用Finish
// 入参: ctx 取消上下文, index 页面索引
// 返回: *Reader 阅读快照, error 转换或构建错误
func (p *PDFImport) Reader(ctx context.Context, index int) (*Reader, error) {
if p.importer == nil {
return nil, fmt.Errorf("PDF import is closed")
}
if p.err != nil {
return nil, p.err
}
if index != -1 {
if err := p.ConvertPage(ctx, index); err != nil {
return nil, err
} }
return ctx.Err() }
}) snapshot := *p.importer.editor
snapshot.pages = slices.Clone(p.pages)
if index >= 0 {
snapshot.pages[index] = p.importer.editor.pages[index]
}
reader, err := snapshot.Reader(ctx)
if err != nil { if err != nil {
return nil, PDFImportReport{}, err return nil, err
} }
if importer.report.Pages == 0 { reader.pageReady = make(map[string]bool)
return nil, PDFImportReport{}, fmt.Errorf("PDF contains no pages") if index >= 0 {
reader.pageReady[reader.doc.Pages.Page[index].ID] = true
} }
if err := importer.documentStructures(ctx); err != nil { return reader, nil
return nil, PDFImportReport{}, fmt.Errorf("import PDF structure: %w", err) }
// Finish 转换尚未就绪的页面并补全文档结构,返回独立编辑文档
// 入参: ctx 取消上下文
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 转换错误
func (p *PDFImport) Finish(ctx context.Context) (*Editor, PDFImportReport, error) {
if p.importer == nil {
return nil, PDFImportReport{}, fmt.Errorf("PDF import is closed")
} }
return editor, importer.report, nil for index := range p.pages {
if err := p.ConvertPage(ctx, index); err != nil {
return nil, PDFImportReport{}, err
}
}
if !p.finished {
p.importer.ctx = p.importer.resources.context(ctx)
if err := p.importer.documentStructures(p.importer.ctx); err != nil {
p.err = err
return nil, PDFImportReport{}, fmt.Errorf("import PDF structure: %w", err)
}
p.finished = true
}
return p.importer.editor, p.importer.report, nil
}
// Reset 放弃已转换内容并重建索引,保留文档标识及输出加密策略
// 失败时保留原会话,已交付的阅读快照不受影响;输入源仍由调用方持有
// 入参: ctx 本次重建的取消上下文
// 返回: error 重建错误
func (p *PDFImport) Reset(ctx context.Context) error {
if p.importer == nil {
return fmt.Errorf("PDF import is closed")
}
next, err := NewPDFImport(ctx, p.input, p.options)
if err != nil {
return err
}
next.importer.editor.Info.DocID = p.importer.editor.Info.DocID
next.importer.editor.encryption = p.importer.editor.encryption
p.Close()
*p = *next
return nil
}
// Close 释放PDF索引、引用缓存和转换状态,不关闭调用方的输入源
// 返回: error 关闭错误
func (p *PDFImport) Close() error {
if p.importer == nil {
return nil
}
p.importer.references.Close()
err := p.importer.reader.Close()
p.importer = nil
p.pages, p.ready = nil, nil
clear(p.options.Password)
p.options = PDFImportOptions{}
p.input = nil
return err
} }
// ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标 // ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标
+9 -1
View File
@@ -20,6 +20,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"maps" "maps"
@@ -28,6 +29,9 @@ import (
"strings" "strings"
) )
// ErrIncompleteDocument 表示渐进阅读快照尚未包含请求的页面或完整文档
var ErrIncompleteDocument = errors.New("document content is incomplete")
// Reader OFD文件阅读器 // Reader OFD文件阅读器
// Reader及共享它的Renderer需串行调用 // Reader及共享它的Renderer需串行调用
type Reader struct { type Reader struct {
@@ -50,6 +54,7 @@ type Reader struct {
drawParamCache map[string]*DrawParam drawParamCache map[string]*DrawParam
compositeGraphicUnitCache map[string]*CompositeGraphicUnit compositeGraphicUnitCache map[string]*CompositeGraphicUnit
pageHeaderCache map[string]PageContent pageHeaderCache map[string]PageContent
pageReady map[string]bool
pageContents *renderCache[pageContentKey, PageContent] pageContents *renderCache[pageContentKey, PageContent]
doc *Document doc *Document
relatedPending bool relatedPending bool
@@ -211,7 +216,7 @@ func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error
if index < 0 || index >= r.DocumentCount() { if index < 0 || index >= r.DocumentCount() {
return nil, fmt.Errorf("document index %d out of range", index) return nil, fmt.Errorf("document index %d out of range", index)
} }
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index, resources: r.resources} next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index, resources: r.resources, pageReady: r.pageReady}
if err := next.initRootContext(ctx); err != nil { if err := next.initRootContext(ctx); err != nil {
return nil, err return nil, err
} }
@@ -1098,6 +1103,9 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取 // 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
// 返回: *PageContent 页面内容, error 错误信息 // 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, template bool) (*PageContent, error) { func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, template bool) (*PageContent, error) {
if !template && r.pageReady != nil && !r.pageReady[page.ID] {
return nil, ErrIncompleteDocument
}
fullPath := r.ResPath(page.BaseLoc) fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFileViewContext(ctx, fullPath) data, err := r.readFileViewContext(ctx, fullPath)
if err != nil { if err != nil {
+1 -1
View File
@@ -113,7 +113,7 @@ func (r *Reader) DocumentVersion(id string) (*Reader, error) {
// 入参: ctx 取消上下文, id 版本标识,空值选择主入口 // 入参: ctx 取消上下文, id 版本标识,空值选择主入口
// 返回: *Reader 版本阅读器, error 读取或取消错误 // 返回: *Reader 版本阅读器, error 读取或取消错误
func (r *Reader) DocumentVersionContext(ctx context.Context, id string) (*Reader, error) { func (r *Reader) DocumentVersionContext(ctx context.Context, id string) (*Reader, error) {
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id} next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id, pageReady: r.pageReady}
if err := next.initRootContext(ctx); err != nil { if err := next.initRootContext(ctx); err != nil {
return nil, err return nil, err
} }