mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
04d7d2d672
commit
fb4f03465b
11 files changed
+608
-115
No files matched your search
+50
-28
@@ -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) {
|
||||||
|
|||||||
+41
-11
@@ -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) {
|
||||||
@@ -92,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; }
|
||||||
@@ -307,6 +307,8 @@ class BrowserEngine {
|
|||||||
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;
|
||||||
@@ -323,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 || ""};
|
||||||
@@ -347,6 +350,7 @@ 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;
|
||||||
@@ -358,6 +362,7 @@ class BrowserEngine {
|
|||||||
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();
|
||||||
@@ -378,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;
|
||||||
@@ -389,7 +400,9 @@ 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;
|
if (this.slots.has(slot)) this.startupTime = performance.now() - started;
|
||||||
@@ -409,6 +422,7 @@ class BrowserEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageSlot(data) {
|
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 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));
|
const pending = [...this.requests.values()].filter(slot => slot === null).length - Number(this.requests.has(data.id));
|
||||||
if (!preferred && pending <= 0) return null;
|
if (!preferred && pending <= 0) return null;
|
||||||
@@ -440,6 +454,14 @@ class BrowserEngine {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -451,7 +473,7 @@ class BrowserEngine {
|
|||||||
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",
|
||||||
@@ -472,11 +494,19 @@ class BrowserEngine {
|
|||||||
|
|
||||||
renderSlot(slot, data, transfer) {
|
renderSlot(slot, data, transfer) {
|
||||||
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);
|
||||||
@@ -612,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;
|
||||||
}
|
}
|
||||||
@@ -626,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;
|
||||||
@@ -688,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);
|
||||||
}
|
}
|
||||||
@@ -721,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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,10 +337,21 @@ 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 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 (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;
|
||||||
|
|||||||
+241
-35
@@ -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 != 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
|
return nil, err
|
||||||
}
|
}
|
||||||
warnings, err := json.Marshal(report.Warnings)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 使用库转换独立文件,不读取或修改当前阅读和编辑会话
|
// 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 },
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+227
-33
@@ -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,28 +345,94 @@ 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 {
|
||||||
|
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)
|
matrix, width, height := codecpdf.PageMatrix(page)
|
||||||
importer.matrix = matrix
|
importer.matrix = matrix
|
||||||
importer.page = index
|
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 {
|
if err := importer.pageOpenActions(ctx, page); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
|
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)
|
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)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if err := importer.commitAnnotations(); err != nil {
|
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)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
importer.report.Pages++
|
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
|
return err
|
||||||
}
|
}
|
||||||
if options.Progress != nil {
|
if p.options.Progress != nil {
|
||||||
if err := options.Progress(index + 1); err != nil {
|
if err := p.options.Progress(importer.report.Pages); err != nil {
|
||||||
return err
|
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 {
|
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
|
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 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,转换阶段失败时不写入目标
|
// ConvertPDF 将PDF转换并写入OFD,转换阶段失败时不写入目标
|
||||||
|
|||||||
+9
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user