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