feat(强化加载): 支持PDF按页加载与增量转换
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-26 17:21:15 +08:00
1 parent 04d7d2d672
commit fb4f03465b
11 files changed
+608 -115

No files matched your search

+50 -28
View File
@@ -116,6 +116,8 @@ function createDocumentState() {
textFontID: null,
textDefaults: { type: "TextObject", size: 16 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 },
ofdBytes: null,
sourcePDF: false,
pdfComplete: false,
fileName: "ofdgo.ofd",
openSeq: 0,
pageSeq: 0,
@@ -5503,11 +5505,10 @@ async function loadWASM(tab = documentTab()) {
if (data.type === "progress") {
if (visible) setProgress(data.text, data.percent);
} 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);
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 === "write.fonts") setProgress("正在处理字体", null);
else setProgress("正在生成文档", null);
}
} else if (data.type === "export") {
@@ -5723,7 +5724,6 @@ async function openOFD(file, remote = null, handle = file?.handle) {
workspace.tabs.push(tab);
const current = () => !tab.closed && tab.opening === request;
let accepted = false;
let originalSource = null;
try {
setBusy(true, "正在读取文档", 10, STATUS.opening, tab);
await activateDocumentTab(tab);
@@ -5742,11 +5742,10 @@ async function openOFD(file, remote = null, handle = file?.handle) {
const source = downloaded ? downloaded.file : file;
const format = await callTabWASM(tab, "ofdgoDetectFormat", source);
if (!current()) return;
let bytes = await documentBytes(source);
originalSource = bytes;
const bytes = await documentBytes(source);
tab.state.ofdBytes = bytes;
tab.state.sourcePDF = format === "pdf";
const name = (downloaded ? downloaded.name : file.name) || `ofdgo.${format}`;
let warnings = [];
let convertedDoc;
if (!current()) {
return;
@@ -5765,10 +5764,7 @@ async function openOFD(file, remote = null, handle = file?.handle) {
}
}
if (!current()) return;
bytes = converted.bytes;
tab.state.ofdBytes = bytes;
convertedDoc = converted.doc;
warnings = JSON.parse(converted.warnings || "null") || [];
convertedDoc = converted;
}
await ensureTabEngine(tab);
if (!current()) return;
@@ -5776,7 +5772,7 @@ async function openOFD(file, remote = null, handle = file?.handle) {
if (!current()) return;
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.conversionWarnings = warnings;
tab.state.conversionWarnings = [];
tab.state.fileName = (name || "ofdgo.ofd").replace(/\.pdf$/i, ".ofd");
if (remote && !/\.ofd$/i.test(tab.state.fileName)) tab.state.fileName += ".ofd";
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;
}
else tab.status = "文档已打开";
if (tab.state.sourcePDF) completePDF(tab);
} catch (err) {
if (current()) {
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);
tab.state.ofdBytes = null;
}
if (nativeHost && originalSource !== source) await nativeHost.release(originalSource);
}
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()) {
const state = tab.state;
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
pdf ||= !importing && state.sourcePDF;
const current = () => !tab.closed && (Boolean(tab.opening) || openSeq === state.openSeq);
let credentials = null;
try {
while (current()) {
try {
if (pdf) {
const converted = await callWASM("ofdgoConvertPDF", data, fonts, credentials?.password || null);
let accepted = false;
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);
}
state.pdfComplete = false;
return await callWASM("ofdgoOpenPDF", data, fonts, credentials?.password || null, state.renderAnnotations);
}
return importing ? await callWASM("ofdgoLoadImport", data?.nativeFile ? data : data.slice(), credentials)
: await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials);
@@ -6444,8 +6433,10 @@ async function openDocument(options = {}) {
if (openSeq !== state.openSeq) {
return;
}
wasmWorker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? {
file: state.ofdBytes, fonts, annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion
const pageFonts = state.sourcePDF && !nativeHost && !state.editorInfo ? await fontManager.files(fontManager.records()) : fonts;
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);
if (options.keepPreview) {
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) {
const tab = documentTab(), state = tab.state;
const callWASM = (name, ...args) => callTabWASM(tab, name, ...args);
if (openSeq !== state.openSeq || !state.doc) return;
if (state.sourcePDF && !state.pdfComplete && !state.editorInfo) { completePDF(tab); return; }
try {
const details = await callWASM("ofdgoDocumentInfo");
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;
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;
if (saving && indices === null && !readingSave) {
snapshotPrepared = true;
@@ -12350,13 +12371,13 @@ async function callTabWASM(tab, name, ...args) {
if (name === "ofdgoResources" && workspace.active === tab && resourceElements.Panel.open) resourceManager.requestID = id;
wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq, tab });
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
if (name === "ofdgoConvertPDF") {
if (name === "ofdgoOpenPDF" || state.sourcePDF && !state.editorInfo) {
const request = wasmRequests.get(id);
request.localFonts = fontManager.localEntries();
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;
if (visible) {
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);
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;
tab.worker.postMessage(message, transfer);
} catch (err) {
+41 -11
View File
@@ -35,7 +35,7 @@ class BrowserScheduler {
}
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) {
@@ -92,8 +92,8 @@ class BrowserScheduler {
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 active = this.active();
const interactive = task.worker.primary && this.owner(task.worker, task.data)?.foreground === true && !task.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));
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 && !data.background));
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;
if (busy || snapshot || !available || !task.worker.primary && active && this.memory + (64 << 20) > this.budget) { i++; continue; }
@@ -307,6 +307,8 @@ class BrowserEngine {
this.primary.onerror = event => this.onerror?.(event);
this.primary.onmessageerror = event => this.onmessageerror?.(event);
this.requests = new Map();
this.fontRequests = new Map();
this.primaryPages = new Set();
this.slots = new Set();
this.sequence = 0;
this.revision = 0;
@@ -323,8 +325,9 @@ class BrowserEngine {
configurePages(source) {
const current = this.pageSource;
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);
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();
if (!source || this.closed) return;
this.pageSource = {...source, fonts, document:source.document || 0, version:source.version || ""};
@@ -347,6 +350,7 @@ class BrowserEngine {
invalidatePages() {
this.revision++;
this.primaryPages.clear();
this.source = null;
this.pageSource = null;
this.parallelism = 1;
@@ -358,6 +362,7 @@ class BrowserEngine {
closeSlot(slot, error = new DOMException("页面加载已取消", "AbortError")) {
if (!this.slots.delete(slot)) return;
slot.worker.terminate();
for (const [id, request] of this.fontRequests) if (request.worker === slot.worker) this.fontRequests.delete(id);
slot.rejectReady(error);
for (const pending of slot.pending.values()) pending.reject(error);
slot.pending.clear();
@@ -378,6 +383,12 @@ class BrowserEngine {
if (this.source) this.parallelism = engineScheduler.limit;
if (data.type === "ready") { resolve(); 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;
const request = slot.pending.get(data.id);
if (!request) return;
@@ -389,7 +400,9 @@ class BrowserEngine {
});
this.slots.add(slot);
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.version || "") !== (opened.data.documentVersion || "")) await this.callSlot(slot, "ofdgoSelectVersion", [source.version || ""]);
if (this.slots.has(slot)) this.startupTime = performance.now() - started;
@@ -409,6 +422,7 @@ 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;
@@ -440,6 +454,14 @@ class BrowserEngine {
postMessage(data, transfer) {
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) {
this.onmessage?.({data:{engineID:this.engineID, id:data.id, ok:false, canceled:true, error:"预加载已暂停"}});
return;
@@ -451,7 +473,7 @@ class BrowserEngine {
return;
}
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",
"ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles",
"ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures",
@@ -472,11 +494,19 @@ class BrowserEngine {
renderSlot(slot, data, transfer) {
slot.busy = true;
slot.requestID = data.id;
this.requests.set(data.id, slot);
const revision = this.revision;
let fallback = false;
slot.ready.then(() => this.callSlot(slot, data.name, data.args, !!data.preload)).then(result => {
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.pages.delete(slot.lastPage);
slot.pages.add(slot.lastPage);
@@ -612,13 +642,13 @@ class NativeHost {
}
}
async decode(value, name = "", key = "", signal) {
async decode(value, 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);
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))));
return;
}
@@ -626,7 +656,7 @@ class NativeHost {
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 failed = results.find(result => result.status === "rejected");
if (failed) throw failed.reason;
@@ -688,7 +718,7 @@ class NativeHost {
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];
if (input?.nativeFile && !input.temporary) inputs.add(input.nativeFile);
}
@@ -721,7 +751,7 @@ class NativeHost {
let decoded;
try {
controller.signal.throwIfAborted();
decoded = await host.decode(result, data.name, "", controller.signal);
decoded = await host.decode(result, controller.signal);
controller.signal.throwIfAborted();
}
catch (error) { await host.release(result); throw error; }
+1 -1
View File
@@ -229,7 +229,7 @@ export function createPageView(context) {
for (const task of context.state.pagePresentQueue.values()) task.resolve(null);
context.state.pagePresentQueue.clear();
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()) {
task.resolve(null);
+14 -3
View File
@@ -235,9 +235,9 @@ async function handleMessage(engine, { id, name, args }) {
let saveStamp;
let channel;
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 converting = name === "ofdgoConvertPDF";
const converting = name === "ofdgoOpenPDF";
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSnapshot" || name === "ofdgoSaveSigned";
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;
@@ -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;
try {
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();
args[0] = {size: file.size, read: (offset, size) => new Uint8Array(reader.readAsArrayBuffer(file.slice(offset, offset + size)))};
}
@@ -337,10 +337,21 @@ async function handleMessage(engine, { id, name, args }) {
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);
signal?.throwIfAborted();
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) {
result.data.size = size;
+241 -35
View File
@@ -32,6 +32,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xiaoqidun/ofdgo"
@@ -435,15 +436,30 @@ type outputContext struct {
cancel context.CancelFunc
checkpoint func() error
checked time.Time
pending uint8
checks atomic.Uint32
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 独立文档、编辑、剪贴板及操作上下文
type businessEngine struct {
resources *ofdgo.Resources
currentSession *Session
currentEditor *ofdgo.Editor
pdf *pendingPDF
pdfWarnings []byte
saveSnapshot *ofdgo.Editor
printDocument *ofdgo.PrintDocument
pendingImport *ofdgo.Reader
@@ -1394,58 +1410,243 @@ func (e *businessEngine) detectDocumentFormat(args []platformValue) (any, error)
return ofdgo.DetectFormat(input)
}
// convertPDFDocument 通过库转换PDF,不替换当前文档会话
// 入参: args PDF数据、外部字体、UTF-8密码、字体目录、进度及字体读取回调
// 返回: any OFD数据及转换警告, error 错误信息
func (e *businessEngine) convertPDFDocument(args []platformValue) (any, error) {
if len(args) != 6 {
return nil, fmt.Errorf("missing PDF data")
// clearPDF 释放当前标签的未完成转换和输入
func (e *businessEngine) clearPDF() {
e.pdfWarnings = nil
if e.pdf == nil {
return
}
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])
if err != nil {
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])
if err != nil {
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() {
options.Password, err = e.bytesFromJS(args[2])
password, err = e.bytesFromJS(args[2])
if err != nil {
return nil, err
}
defer clear(options.Password)
password = slices.Clone(password)
}
fontSources := []fs.FS{ofdgo.NewFontFS(fonts)}
if names := e.stringsFromJS(args[3]); len(names) > 0 {
fontSources = append(fontSources, ofdgo.NewLazyFontFS(names, func(name string) ([]byte, error) {
return e.loadConversionFont(args[5], name)
pending.operation, pending.report = e.newEditorOperationContext(pending.progress)
options := ofdgo.PDFImportOptions{Resources: e.resources, PasswordUTF8: true, Password: password,
OnProgress: func(stage string, completed, total int) error {
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...)}
editor, report, err := ofdgo.ImportPDF(e.operationContext(), input, options)
options.RendererOptions = []ofdgo.RendererOption{ofdgo.WithFontFS(sources...)}
pending.importer, err = ofdgo.NewPDFImport(pending.operation, input, options)
if err != nil {
return nil, err
}
if len(options.Password) != 0 {
if err := editor.SetEncryption(&ofdgo.EncryptionOptions{Password: options.Password}); err != nil {
if len(password) != 0 {
if err := pending.importer.SetEncryption(&ofdgo.EncryptionOptions{Password: password}); err != nil {
return nil, err
}
}
editor.OnWriteProgress = func(stage string, completed, total int) error {
return options.OnProgress("write."+stage, completed, total)
reader, err := pending.importer.Reader(pending.operation, -1)
if err != nil {
return nil, err
}
if _, err := editor.WriteTo(&output); err != 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 successResult(map[string]any{"bytes": bytesToJS(output.Bytes()), "warnings": string(warnings)}), nil
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 使用库转换独立文件,不读取或修改当前阅读和编辑会话
@@ -1641,6 +1842,7 @@ func (e *businessEngine) openDocument(args []platformValue) (any, error) {
if err != nil {
return nil, err
}
e.clearPDF()
e.clearImport()
e.currentEditor = nil
e.copiedObjects = nil
@@ -1689,6 +1891,9 @@ func (e *businessEngine) configureDocument(args []platformValue) (any, error) {
// 入参: args 浏览器参数
// 返回: 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 {
return nil, fmt.Errorf("ofd document is not opened")
}
@@ -7418,8 +7623,8 @@ func (e *businessEngine) newOutputContext(checkpoint platformValue) *outputConte
// newEditorOperationContext 在底层取消检查时保持最近的进度,避免版本处理期间界面无响应
// 入参: callback 浏览器进度及取消回调
// 返回: context.Context 操作上下文, func(string, int, int) error 阶段进度回调
func (e *businessEngine) newEditorOperationContext(callback platformValue) (context.Context, func(string, int, int) error) {
// 返回: *outputContext 操作上下文, func(string, int, int) error 阶段进度回调
func (e *businessEngine) newEditorOperationContext(callback platformValue) (*outputContext, func(string, int, int) error) {
ctx, cancel := context.WithCancel(e.operationContext())
stage, completed, total := "ids", 0, 0
progress := e.editorOperationProgress(callback)
@@ -7436,16 +7641,15 @@ func (c *outputContext) Err() error {
if err := c.Context.Err(); err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
if c.pending != 0 {
c.pending--
interval := uint32(c.interval) + 1
if c.interval == 0 {
interval = 16
}
if (c.checks.Add(1)-1)%interval != 0 {
return nil
}
c.pending = 15
if c.interval != 0 {
c.pending = c.interval
}
c.mu.Lock()
defer c.mu.Unlock()
if time.Since(c.checked) >= 50*time.Millisecond {
c.checked = time.Now()
if err := c.checkpoint(); err != nil {
@@ -7465,6 +7669,9 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoDiscardPrint": e.discardPrint,
"ofdgoDetectFormat": e.detectDocumentFormat,
"ofdgoOpen": e.openDocument,
"ofdgoOpenPDF": e.openPDF,
"ofdgoLoadPDF": e.loadPDF,
"ofdgoPDFContext": e.pdfContext,
"ofdgoSelectDocument": e.selectDocument,
"ofdgoDocumentVersions": e.documentVersions,
"ofdgoExportVersion": e.exportVersion,
@@ -7488,7 +7695,6 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
"ofdgoChangeVersion": e.changeDocumentVersion,
"ofdgoChangeDocument": e.changePackageDocument,
"ofdgoPackageDocuments": e.packageDocuments,
"ofdgoConvertPDF": e.convertPDFDocument,
"ofdgoConvertFile": e.convertFile,
"ofdgoPackFiles": e.packFiles,
"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 {
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() {
if failure := recover(); failure != nil {
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))
}
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() {
if err := call.abort(); err != nil {
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 {
values[i] = platformValueOf(arg)
}
if err := e.preparePDF(name, values); err != nil {
return nativeFailure(err)
}
data, err := e.safeCall(fn, values)
if err != nil {
return nativeFailure(err)
@@ -357,6 +364,7 @@ func (e *NativeEngine) Close() error {
e.gate <- struct{}{}
defer func() { <-e.gate }()
e.closed = true
e.clearPDF()
_, _ = e.discardPrint(nil)
_ = e.waitRenders(context.Background())
e.renderIdle = nil
@@ -955,7 +963,7 @@ func (c *nativeCall) prepare(name string, args []any) ([]any, error) {
return append(args, checkpoint), nil
case "ofdgoImportPages", "ofdgoImportDocuments", "ofdgoImportImages":
return append(args, c.progress("import", "", true)), nil
case "ofdgoConvertPDF":
case "ofdgoOpenPDF":
return append(args, c.progress("conversion", "", true), nativeCallback(c.font)), nil
case "ofdgoExportImages", "ofdgoExportPage", "ofdgoExportDocument", "ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion", "ofdgoSaveDocument", "ofdgoSaveSigned":
if len(args) == 0 {
+11
View File
@@ -79,10 +79,20 @@ func (w *wasmEngines) call(args []js.Value) (any, error) {
if e == nil {
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()]
if fn == nil {
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:])
}
@@ -100,6 +110,7 @@ func (w *wasmEngines) close(args []js.Value) (any, error) {
}
delete(w.engines, id)
_, _ = e.discardPrint(nil)
e.clearPDF()
e.clearImport()
err := e.currentSession.Close()
*e = businessEngine{}
+3
View File
@@ -131,6 +131,9 @@ func (r *Reader) Editor() (*Editor, error) {
// 入参: ctx 取消上下文
// 返回: *Editor 编辑器, error 初始化或取消错误
func (r *Reader) EditorContext(ctx context.Context) (*Editor, error) {
if r.pageReady != nil {
return nil, ErrIncompleteDocument
}
_, err := r.DocContext(ctx)
if err != nil {
return nil, err
+227 -33
View File
@@ -69,6 +69,25 @@ type PDFImportReport struct {
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 保存单次转换的对象与资源状态
type pdfImporter struct {
resources *Resources
@@ -212,21 +231,34 @@ func (o pdfObjectOrigin) merge(other pdfObjectOrigin) pdfObjectOrigin {
// 入参: ctx 取消上下文, input 输入源, options 转换选项
// 返回: *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 {
options.Resources, _ = NewResources(ResourceOptions{})
}
ctx = options.Resources.context(ctx)
if input == nil {
return nil, PDFImportReport{}, fmt.Errorf("missing source")
return nil, fmt.Errorf("missing source")
}
if err := ctx.Err(); err != nil {
return nil, PDFImportReport{}, err
return nil, err
}
if err := options.Compression.Validate(); err != nil {
return nil, PDFImportReport{}, err
return nil, err
}
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...)
if options.Backends != nil {
@@ -236,18 +268,25 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
renderer.DPI = options.RasterDPI
}
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)
if err := progress.report("open", 0, 0); err != nil {
return nil, PDFImportReport{}, err
return nil, err
}
editor := NewEditor()
editor.runtimeResources = options.Resources
editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources()
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}
if !options.Strict {
importer.warning = func(warning pdfgo.Diagnostic) {
@@ -256,22 +295,26 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
}
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 {
return nil, PDFImportReport{}, err
return nil, err
}
defer reader.Close()
defer func() {
if !keep {
reader.Close()
}
}()
importer.reader = reader
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()
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 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"})
}
@@ -281,10 +324,14 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
importer.halftones = options.Halftones
importer.compression = options.Compression
importer.references = &codecpdf.ReferenceCache{}
defer importer.references.Close()
defer func() {
if !keep {
importer.references.Close()
}
}()
importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
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 {
_, width, height := codecpdf.PageMatrix(page)
@@ -298,28 +345,94 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
return progress.report("pages", index+1, 0)
})
if err != nil {
return nil, PDFImportReport{}, err
return nil, err
}
if len(editor.pages) != 0 {
importer.page = -1
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 {
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 {
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 {
return nil, PDFImportReport{}, fmt.Errorf("import PDF document actions: %w", err)
return nil, fmt.Errorf("import PDF document actions: %w", err)
}
importer.page = 0
}
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 {
return nil, fmt.Errorf("PDF contains no pages")
}
keep = true
return &PDFImport{input: input, importer: importer, options: options, pages: cloneEditorData(editor.pages), ready: make([]bool, len(editor.pages))}, nil
}
// ReadAt 读取当前操作需要的数据,不取得输入所有权
// 入参: data 目标缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取或取消错误
func (r *pdfImportInput) ReadAt(data []byte, offset int64) (int, error) {
if err := r.importer.ctx.Err(); err != nil {
return 0, err
}
return r.input.ReadAt(data, offset)
}
// PageCount 返回文档页数
// 返回: int 页面总数
func (p *PDFImport) PageCount() int { return len(p.pages) }
// 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
}
}()
importer := p.importer
importer.ctx = importer.resources.context(ctx)
ctx = importer.ctx
importer.renderer.OutputContext = ctx
page := importer.pageOrder[index]
matrix, width, height := codecpdf.PageMatrix(page)
importer.matrix = matrix
importer.page = index
@@ -341,10 +454,10 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
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 reader.WalkPage(ctx, page, visitor) }); err != nil {
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, options.Strict); err != nil {
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 {
@@ -354,26 +467,107 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
importer.report.Pages++
if err := progress.report("convert", index+1, len(editor.pages)); err != nil {
if err := editorProgress(p.options.OnProgress).withContext(ctx).report("convert", importer.report.Pages, len(p.pages)); err != nil {
return err
}
if options.Progress != nil {
if err := options.Progress(index + 1); err != nil {
if p.options.Progress != nil {
if err := p.options.Progress(importer.report.Pages); err != nil {
return err
}
}
return ctx.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
}
}
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 {
return nil, err
}
reader.pageReady = make(map[string]bool)
if index >= 0 {
reader.pageReady[reader.doc.Pages.Page[index].ID] = true
}
return reader, nil
}
// 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")
}
for index := range p.pages {
if err := p.ConvertPage(ctx, index); err != nil {
return nil, PDFImportReport{}, err
}
if importer.report.Pages == 0 {
return nil, PDFImportReport{}, fmt.Errorf("PDF contains no pages")
}
if err := importer.documentStructures(ctx); err != nil {
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)
}
return editor, importer.report, nil
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,转换阶段失败时不写入目标
+9 -1
View File
@@ -20,6 +20,7 @@ import (
"context"
"crypto/sha256"
"encoding/xml"
"errors"
"fmt"
"io"
"maps"
@@ -28,6 +29,9 @@ import (
"strings"
)
// ErrIncompleteDocument 表示渐进阅读快照尚未包含请求的页面或完整文档
var ErrIncompleteDocument = errors.New("document content is incomplete")
// Reader OFD文件阅读器
// Reader及共享它的Renderer需串行调用
type Reader struct {
@@ -50,6 +54,7 @@ type Reader struct {
drawParamCache map[string]*DrawParam
compositeGraphicUnitCache map[string]*CompositeGraphicUnit
pageHeaderCache map[string]PageContent
pageReady map[string]bool
pageContents *renderCache[pageContentKey, PageContent]
doc *Document
relatedPending bool
@@ -211,7 +216,7 @@ func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error
if index < 0 || index >= r.DocumentCount() {
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 {
return nil, err
}
@@ -1098,6 +1103,9 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
// 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
// 返回: *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)
data, err := r.readFileViewContext(ctx, fullPath)
if err != nil {
+1 -1
View File
@@ -113,7 +113,7 @@ func (r *Reader) DocumentVersion(id string) (*Reader, error) {
// 入参: ctx 取消上下文, id 版本标识,空值选择主入口
// 返回: *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 {
return nil, err
}