mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1 parent
010ab9f314
commit
66aa87304b
10 files changed
+355
-69
No files matched your search
+17
-14
@@ -4128,6 +4128,7 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function releasePageResources() {
|
function releasePageResources() {
|
||||||
|
if (state.pageInFlight.size) return;
|
||||||
const images = new Set(), fonts = new Set();
|
const images = new Set(), fonts = new Set();
|
||||||
for (const page of state.pageCache.values()) {
|
for (const page of state.pageCache.values()) {
|
||||||
for (const name of page.imageNames) images.add(name);
|
for (const name of page.imageNames) images.add(name);
|
||||||
@@ -4568,7 +4569,7 @@ async function loadWASM() {
|
|||||||
state.ready = false;
|
state.ready = false;
|
||||||
state.wasmExited = false;
|
state.wasmExited = false;
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const worker = createEngineWorker();
|
const worker = createEngineWorker({pages: true});
|
||||||
wasmWorker = worker;
|
wasmWorker = worker;
|
||||||
const fail = (err) => {
|
const fail = (err) => {
|
||||||
markWASMExited(wasmSeq, err);
|
markWASMExited(wasmSeq, err);
|
||||||
@@ -5473,6 +5474,9 @@ async function openDocument(options = {}) {
|
|||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
wasmWorker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? {
|
||||||
|
file: state.ofdBytes, fonts, annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion
|
||||||
|
} : null);
|
||||||
if (options.keepPreview) {
|
if (options.keepPreview) {
|
||||||
resetPageLoading();
|
resetPageLoading();
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
@@ -8724,7 +8728,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value,
|
|||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
const result = readingSave
|
const result = readingSave
|
||||||
? { label: "OFD", size: state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
? { label: "OFD", size: state.ofdBytes.size ?? state.ofdBytes.byteLength, mime, blob: state.ofdBytes }
|
||||||
: saving ? await callWASM(background ? "ofdgoSaveSnapshot" : "ofdgoSaveDocument", indices, ...outputOptions, destination) : whole
|
: saving ? await callWASM(background ? "ofdgoSaveSnapshot" : "ofdgoSaveDocument", indices, ...outputOptions, destination) : whole
|
||||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, ...outputOptions, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, ...outputOptions, file);
|
||||||
@@ -8971,11 +8975,9 @@ function resetPageFlow(keepCache = false) {
|
|||||||
|
|
||||||
function resetPageLoading() {
|
function resetPageLoading() {
|
||||||
releaseMediaPlayers();
|
releaseMediaPlayers();
|
||||||
if (nativeHost) {
|
|
||||||
for (const [id, request] of wasmRequests) {
|
for (const [id, request] of wasmRequests) {
|
||||||
if (request.name === "ofdgoRenderPage") wasmWorker?.postMessage({ type: "cancel", id });
|
if (request.name === "ofdgoRenderPage") wasmWorker?.postMessage({ type: "cancel", id });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (const task of state.pageInFlight.values()) {
|
for (const task of state.pageInFlight.values()) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
}
|
}
|
||||||
@@ -9088,9 +9090,10 @@ function loadPageData(index, options = {}) {
|
|||||||
const current = state.pageInFlight.get(key);
|
const current = state.pageInFlight.get(key);
|
||||||
if (current) {
|
if (current) {
|
||||||
current.priority = Math.min(current.priority, priority);
|
current.priority = Math.min(current.priority, priority);
|
||||||
|
if (current.preview && !options.preview) return current.promise.then(() => loadPageData(index, options));
|
||||||
return current.promise;
|
return current.promise;
|
||||||
}
|
}
|
||||||
if (!options.refresh && state.pageCache.has(index)) {
|
if (!options.refresh && state.pageCache.has(index) && (options.preview || !state.pageCache.get(index).previewOnly)) {
|
||||||
const page = state.pageCache.get(index);
|
const page = state.pageCache.get(index);
|
||||||
state.pageCache.delete(index);
|
state.pageCache.delete(index);
|
||||||
state.pageCache.set(index, page);
|
state.pageCache.set(index, page);
|
||||||
@@ -9102,7 +9105,7 @@ function loadPageData(index, options = {}) {
|
|||||||
resolve = done;
|
resolve = done;
|
||||||
reject = fail;
|
reject = fail;
|
||||||
});
|
});
|
||||||
const task = { key, index, openSeq, priority, refresh: options.refresh, resolve, reject, promise };
|
const task = { key, index, openSeq, priority, preview: !!options.preview, refresh: options.refresh, resolve, reject, promise };
|
||||||
state.pageInFlight.set(key, task);
|
state.pageInFlight.set(key, task);
|
||||||
state.pageRenderQueue.push(task);
|
state.pageRenderQueue.push(task);
|
||||||
schedulePageRender();
|
schedulePageRender();
|
||||||
@@ -9112,7 +9115,7 @@ function loadPageData(index, options = {}) {
|
|||||||
function schedulePageRender() {
|
function schedulePageRender() {
|
||||||
if (state.pageRenderRunning) {
|
if (state.pageRenderRunning) {
|
||||||
if (state.pageRenderWorkers) {
|
if (state.pageRenderWorkers) {
|
||||||
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || 1)) processPageRenderTasks();
|
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || wasmWorker?.parallelism || 1)) processPageRenderTasks();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -9122,7 +9125,7 @@ function schedulePageRender() {
|
|||||||
|
|
||||||
async function processPageRenderQueue() {
|
async function processPageRenderQueue() {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
await Promise.all(Array.from({ length: nativeHost?.parallelism || 1 }, () => processPageRenderTasks()));
|
await Promise.all(Array.from({ length: nativeHost?.parallelism || wasmWorker?.parallelism || 1 }, () => processPageRenderTasks()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPageRenderTasks() {
|
async function processPageRenderTasks() {
|
||||||
@@ -9139,12 +9142,12 @@ async function processPageRenderTasks() {
|
|||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!task.refresh && state.pageCache.has(task.index)) {
|
if (!task.refresh && state.pageCache.has(task.index) && (task.preview || !state.pageCache.get(task.index).previewOnly)) {
|
||||||
task.resolve(state.pageCache.get(task.index));
|
task.resolve(state.pageCache.get(task.index));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const args = [task.index, state.renderBackend, state.renderDPI, displayMode() === "raster"];
|
const args = [task.index, state.renderBackend, state.renderDPI, displayMode() === "raster"];
|
||||||
if (nativeHost) args.push([...state.svgFonts.keys()]);
|
args.push([...state.svgFonts.keys()], [...state.svgImages.keys()], task.preview);
|
||||||
const page = await callWASM("ofdgoRenderPage", ...args);
|
const page = await callWASM("ofdgoRenderPage", ...args);
|
||||||
if (state.pageInFlight.get(task.key) !== task) {
|
if (state.pageInFlight.get(task.key) !== task) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
@@ -9230,7 +9233,7 @@ function trimPageCache() {
|
|||||||
for (const name of page.imageNames) {
|
for (const name of page.imageNames) {
|
||||||
const count = references.get(name) - 1;
|
const count = references.get(name) - 1;
|
||||||
references.set(name, count);
|
references.set(name, count);
|
||||||
if (count === 0) {
|
if (count === 0 && !state.pageInFlight.size) {
|
||||||
const image = state.svgImages.get(name);
|
const image = state.svgImages.get(name);
|
||||||
bytes -= image.size;
|
bytes -= image.size;
|
||||||
URL.revokeObjectURL(image.url);
|
URL.revokeObjectURL(image.url);
|
||||||
@@ -11197,7 +11200,7 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
|
|||||||
}
|
}
|
||||||
state.thumbnailInFlight.add(key);
|
state.thumbnailInFlight.add(key);
|
||||||
try {
|
try {
|
||||||
const page = await loadPageData(index, { openSeq, priority: 5 });
|
const page = await loadPageData(index, { openSeq, priority: 5, preview: true });
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12547,11 +12550,11 @@ async function callWASM(name, ...args) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const transfer = [];
|
const transfer = [];
|
||||||
if (name === "ofdgoOpen" && !nativeHost) {
|
if (name === "ofdgoOpen" && !nativeHost && ArrayBuffer.isView(args[0])) {
|
||||||
args[0] = args[0].slice();
|
args[0] = args[0].slice();
|
||||||
transfer.push(args[0].buffer);
|
transfer.push(args[0].buffer);
|
||||||
}
|
}
|
||||||
if (name === "ofdgoLoadImport" && args[0] && !nativeHost) transfer.push(args[0].buffer);
|
if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer);
|
||||||
wasmWorker.postMessage({ id, name, args }, transfer);
|
wasmWorker.postMessage({ id, name, args }, transfer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wasmRequests.delete(id);
|
wasmRequests.delete(id);
|
||||||
|
|||||||
+152
-2
@@ -1,7 +1,8 @@
|
|||||||
let engineModule = null;
|
let engineModule = null;
|
||||||
|
|
||||||
export function createEngineWorker() {
|
export function createEngineWorker(options = {}) {
|
||||||
if (nativeHost) return nativeHost.worker();
|
if (nativeHost) return nativeHost.worker();
|
||||||
|
if (options.pages) return new BrowserEngine();
|
||||||
const worker = new Worker("./ofdgo_wasm.js");
|
const worker = new Worker("./ofdgo_wasm.js");
|
||||||
worker.addEventListener("message", event => {
|
worker.addEventListener("message", event => {
|
||||||
if (event.data.type !== "module") return;
|
if (event.data.type !== "module") return;
|
||||||
@@ -13,7 +14,156 @@ export function createEngineWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function documentBytes(file) {
|
export async function documentBytes(file) {
|
||||||
return file?.nativeFile ? file : new Uint8Array(await file.arrayBuffer());
|
if (file?.nativeFile) return file;
|
||||||
|
if (file instanceof Blob) return new Response(file.stream()).blob();
|
||||||
|
return new Uint8Array(await file.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrowserEngine {
|
||||||
|
constructor() {
|
||||||
|
this.primary = createEngineWorker();
|
||||||
|
this.primary.onmessage = event => {
|
||||||
|
const data = event.data;
|
||||||
|
if (data.memory) this.memory = data.memory;
|
||||||
|
if (data.id != null && !data.type) this.requests.delete(data.id);
|
||||||
|
this.onmessage?.(event);
|
||||||
|
};
|
||||||
|
this.primary.onerror = event => this.onerror?.(event);
|
||||||
|
this.primary.onmessageerror = event => this.onmessageerror?.(event);
|
||||||
|
this.requests = new Map();
|
||||||
|
this.slots = new Set();
|
||||||
|
this.sequence = 0;
|
||||||
|
this.revision = 0;
|
||||||
|
this.memory = 0;
|
||||||
|
this.parallelism = 1;
|
||||||
|
this.budget = Math.max(128, Math.min(1024, (navigator.deviceMemory || 2) * 128)) * 1024 * 1024;
|
||||||
|
this.closed = false;
|
||||||
|
this.source = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
configurePages(source) {
|
||||||
|
this.invalidatePages();
|
||||||
|
if (!source || this.closed) return;
|
||||||
|
this.source = {...source, file: source.file instanceof Blob ? source.file : new Blob([source.file])};
|
||||||
|
this.parallelism = Math.max(1, Math.min(2, (navigator.hardwareConcurrency || 2) - 1, Math.floor(this.budget / (256 * 1024 * 1024))));
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidatePages() {
|
||||||
|
this.revision++;
|
||||||
|
this.source = null;
|
||||||
|
this.parallelism = 1;
|
||||||
|
for (const slot of this.slots) this.closeSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSlot(slot, error = new DOMException("页面加载已取消", "AbortError")) {
|
||||||
|
if (!this.slots.delete(slot)) return;
|
||||||
|
slot.worker.terminate();
|
||||||
|
slot.rejectReady(error);
|
||||||
|
for (const pending of slot.pending.values()) pending.reject(error);
|
||||||
|
slot.pending.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSlot(source) {
|
||||||
|
const worker = createEngineWorker();
|
||||||
|
const slot = {worker, pending: new Map(), busy: false, memory: Math.max(128 << 20, this.memory)};
|
||||||
|
const ready = new Promise((resolve, reject) => {
|
||||||
|
slot.rejectReady = reject;
|
||||||
|
worker.onmessage = ({data}) => {
|
||||||
|
if (data.memory) slot.memory = data.memory;
|
||||||
|
if (data.type === "ready") { resolve(); return; }
|
||||||
|
if (data.type === "exit") { this.closeSlot(slot, new Error("页面加载失败")); return; }
|
||||||
|
if (data.type) return;
|
||||||
|
const request = slot.pending.get(data.id);
|
||||||
|
if (!request) return;
|
||||||
|
slot.pending.delete(data.id);
|
||||||
|
if (data.ok) request.resolve(data);
|
||||||
|
else request.reject(Object.assign(new Error(data.error), {code: data.code}));
|
||||||
|
};
|
||||||
|
worker.onerror = worker.onmessageerror = () => this.closeSlot(slot, new Error("页面加载失败"));
|
||||||
|
});
|
||||||
|
this.slots.add(slot);
|
||||||
|
slot.ready = ready.then(async () => {
|
||||||
|
let opened = await this.callSlot(slot, "ofdgoOpen", [source.file, source.fonts, source.annotations]);
|
||||||
|
if ((source.document || 0) !== (opened.data.documentIndex || 0)) opened = await this.callSlot(slot, "ofdgoSelectDocument", [source.document]);
|
||||||
|
if ((source.version || "") !== (opened.data.documentVersion || "")) await this.callSlot(slot, "ofdgoSelectVersion", [source.version || ""]);
|
||||||
|
});
|
||||||
|
slot.ready.catch(() => {});
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
callSlot(slot, name, args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.slots.has(slot)) { reject(new DOMException("页面加载已取消", "AbortError")); return; }
|
||||||
|
const id = --this.sequence;
|
||||||
|
slot.pending.set(id, {resolve, reject});
|
||||||
|
try { slot.worker.postMessage({id, name, args}); }
|
||||||
|
catch (error) { slot.pending.delete(id); reject(error); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(data, transfer) {
|
||||||
|
if (this.closed) return;
|
||||||
|
if (data.type === "cancel") {
|
||||||
|
const slot = this.requests.get(data.id);
|
||||||
|
if (slot) this.closeSlot(slot);
|
||||||
|
else this.primary.postMessage(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.type && data.name !== "ofdgoRenderPage" && ![
|
||||||
|
"ofdgoDocumentInfo", "ofdgoDocumentVersions", "ofdgoDocumentNavigation", "ofdgoOpenLinks", "ofdgoOutlineLinks", "ofdgoRenderBackends", "ofdgoSVGFontData",
|
||||||
|
"ofdgoSearchPage", "ofdgoPageTextString", "ofdgoResources", "ofdgoResourceReferences", "ofdgoResourceTargets",
|
||||||
|
"ofdgoResourceLocations", "ofdgoFontPreview", "ofdgoFontFaces", "ofdgoFontFace", "ofdgoMatchFontFiles",
|
||||||
|
"ofdgoImages", "ofdgoStructureTargets", "ofdgoStructureObjects", "ofdgoPackageDocuments", "ofdgoVerifySignatures",
|
||||||
|
"ofdgoAnnotationInfos", "ofdgoInvoices", "ofdgoInvoice", "ofdgoExportInvoice", "ofdgoValidateInvoice",
|
||||||
|
"ofdgoActionList", "ofdgoPageSettings", "ofdgoReadAnnotation", "ofdgoPreviewSignatureSeal",
|
||||||
|
"ofdgoExportFormats", "ofdgoOutputFormats", "ofdgoParsePageRange", "ofdgoExportPage", "ofdgoExportDocument",
|
||||||
|
"ofdgoExportAttachment", "ofdgoExportResource", "ofdgoExportMedia", "ofdgoExportVersion"
|
||||||
|
].includes(data.name)) this.invalidatePages();
|
||||||
|
if (data.name !== "ofdgoRenderPage" || !this.source || ![...this.requests.values()].includes(null)) {
|
||||||
|
if (data.name === "ofdgoRenderPage") this.requests.set(data.id, null);
|
||||||
|
this.primary.postMessage(data, transfer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let slot = [...this.slots].find(slot => !slot.busy);
|
||||||
|
const memory = this.memory + [...this.slots].reduce((total, item) => total + item.memory, 0);
|
||||||
|
if (!slot && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source);
|
||||||
|
if (!slot) {
|
||||||
|
this.requests.set(data.id, null);
|
||||||
|
this.primary.postMessage(data, transfer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slot.busy = true;
|
||||||
|
this.requests.set(data.id, slot);
|
||||||
|
const revision = this.revision;
|
||||||
|
let fallback = false;
|
||||||
|
slot.ready.then(() => this.callSlot(slot, data.name, data.args)).then(result => {
|
||||||
|
if (revision !== this.revision) throw new DOMException("页面加载已取消", "AbortError");
|
||||||
|
if (!this.closed) this.onmessage?.({data: {...result, id: data.id}});
|
||||||
|
}).catch(error => {
|
||||||
|
if (!this.closed && revision === this.revision && error.name !== "AbortError") {
|
||||||
|
this.closeSlot(slot, error);
|
||||||
|
this.parallelism = Math.max(1, this.slots.size + 1);
|
||||||
|
this.requests.set(data.id, null);
|
||||||
|
fallback = true;
|
||||||
|
this.primary.postMessage(data, transfer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.closed) this.onmessage?.({data: {id: data.id, ok: false, error: error.message, canceled: error.name === "AbortError"}});
|
||||||
|
}).finally(() => {
|
||||||
|
if (!fallback) this.requests.delete(data.id);
|
||||||
|
slot.busy = false;
|
||||||
|
const memory = this.memory + [...this.slots].reduce((total, item) => total + item.memory, 0);
|
||||||
|
if (memory > this.budget) { this.closeSlot(slot); this.parallelism = Math.max(1, this.slots.size + 1); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
if (this.closed) return;
|
||||||
|
this.closed = true;
|
||||||
|
this.invalidatePages();
|
||||||
|
this.primary.terminate();
|
||||||
|
this.requests.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveFilePicker(options) {
|
export function saveFilePicker(options) {
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ async function handleMessage({ 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) {
|
||||||
|
const file = args[0], reader = new FileReaderSync();
|
||||||
|
args[0] = {size: file.size, read: (offset, size) => new Uint8Array(reader.readAsArrayBuffer(file.slice(offset, offset + size)))};
|
||||||
|
}
|
||||||
if (name === "ofdgoDetectFormat") {
|
if (name === "ofdgoDetectFormat") {
|
||||||
const file = args[0], reader = new FileReaderSync();
|
const file = args[0], reader = new FileReaderSync();
|
||||||
channel = new MessageChannel();
|
channel = new MessageChannel();
|
||||||
@@ -299,8 +303,8 @@ async function handleMessage({ id, name, args }) {
|
|||||||
await output.abort();
|
await output.abort();
|
||||||
}
|
}
|
||||||
output = null;
|
output = null;
|
||||||
const transfer = result.data?.bytes ? [result.data.bytes.buffer] : (result.data?.images || []).map((image) => image.bytes.buffer);
|
const transfer = [...new Set([result.data?.bytes, ...(result.data?.images || []).map(image => image.bytes), ...(result.data?.fonts || []).map(font => font.data)].filter(bytes => bytes?.byteLength).map(bytes => bytes.buffer))];
|
||||||
self.postMessage({ id, ...result }, transfer);
|
self.postMessage({ id, ...result, memory: engineMemory.buffer.byteLength }, transfer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await output?.abort().catch(() => {});
|
await output?.abort().catch(() => {});
|
||||||
const result = { id, ok: false, error: err.message, errorName: err.name };
|
const result = { id, ok: false, error: err.message, errorName: err.name };
|
||||||
|
|||||||
+78
-30
@@ -313,6 +313,7 @@ type exportWriter struct {
|
|||||||
|
|
||||||
// browserReaderAt 分块读取浏览器文件,只缓存最近的一个数据块
|
// browserReaderAt 分块读取浏览器文件,只缓存最近的一个数据块
|
||||||
type browserReaderAt struct {
|
type browserReaderAt struct {
|
||||||
|
mu sync.Mutex
|
||||||
read platformValue
|
read platformValue
|
||||||
checkpoint platformValue
|
checkpoint platformValue
|
||||||
size int64
|
size int64
|
||||||
@@ -461,14 +462,14 @@ func (s *Session) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
reader := s.Reader
|
reader := s.Reader
|
||||||
source := s.input
|
input := s.input
|
||||||
*s = Session{}
|
*s = Session{}
|
||||||
var err error
|
var err error
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
err = reader.Close()
|
err = reader.Close()
|
||||||
}
|
}
|
||||||
if source != nil {
|
if input != nil {
|
||||||
err = errors.Join(err, source.Close())
|
err = errors.Join(err, input.Close())
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -744,6 +745,13 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) {
|
|||||||
// 入参: index 页面索引, backend 后端组合,空值沿用会话配置, dpi 光栅分辨率, raster 是否光栅显示
|
// 入参: index 页面索引, backend 后端组合,空值沿用会话配置, dpi 光栅分辨率, raster 是否光栅显示
|
||||||
// 返回: PageSVG 页面结果, error 错误信息
|
// 返回: PageSVG 页面结果, error 错误信息
|
||||||
func (s *Session) RenderPage(index int, backend string, dpi float64, raster bool) (PageSVG, error) {
|
func (s *Session) RenderPage(index int, backend string, dpi float64, raster bool) (PageSVG, error) {
|
||||||
|
return s.renderPage(index, backend, dpi, raster, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderPage 按需生成预览及交互数据,省略调用方已持有的资源字节
|
||||||
|
// 入参: index 页面索引, backend 后端组合, dpi 光栅分辨率, raster 光栅显示, preview 仅预览, known 已持有资源
|
||||||
|
// 返回: PageSVG 页面结果, error 渲染错误
|
||||||
|
func (s *Session) renderPage(index int, backend string, dpi float64, raster, preview bool, known map[string]bool) (PageSVG, error) {
|
||||||
if backend != "" {
|
if backend != "" {
|
||||||
if err := s.SetRenderBackend(backend); err != nil {
|
if err := s.SetRenderBackend(backend); err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
@@ -759,7 +767,7 @@ func (s *Session) RenderPage(index int, backend string, dpi float64, raster bool
|
|||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
renderer := *s.Renderer
|
renderer := *s.Renderer
|
||||||
if s.cachedPageText(index) == nil {
|
if !preview && s.cachedPageText(index) == nil {
|
||||||
renderer.OnPageText = func(_ *ofdgo.PageContent, text *ofdgo.PageText) {
|
renderer.OnPageText = func(_ *ofdgo.PageContent, text *ofdgo.PageText) {
|
||||||
s.cachePageText(index, text)
|
s.cachePageText(index, text)
|
||||||
}
|
}
|
||||||
@@ -767,10 +775,18 @@ func (s *Session) RenderPage(index int, backend string, dpi float64, raster bool
|
|||||||
if raster {
|
if raster {
|
||||||
renderer.DPI = dpi
|
renderer.DPI = dpi
|
||||||
}
|
}
|
||||||
resources, err := renderer.RenderPreview(page, &buf, ofdgo.PreviewOptions{Raster: raster, Objects: !raster && s.editing})
|
resources, err := renderer.RenderPreview(page, &buf, ofdgo.PreviewOptions{Raster: raster, Objects: !raster && s.editing && !preview, KnownResources: known})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
}
|
}
|
||||||
|
clear(s.svgFonts)
|
||||||
|
for i, font := range resources.Fonts {
|
||||||
|
s.svgFonts[font.Name] = font.Data
|
||||||
|
resources.Fonts[i].Data = nil
|
||||||
|
}
|
||||||
|
if preview {
|
||||||
|
return PageSVG{Index: index, Number: index + 1, ID: page.ID, Width: box.W, Height: box.H, SVG: buf.String(), Fonts: resources.Fonts, Images: resources.Images}, nil
|
||||||
|
}
|
||||||
links, err := s.Renderer.PageLinks(page)
|
links, err := s.Renderer.PageLinks(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageSVG{}, err
|
return PageSVG{}, err
|
||||||
@@ -790,11 +806,6 @@ func (s *Session) RenderPage(index int, backend string, dpi float64, raster bool
|
|||||||
annotations[i].Bounds, annotations[i].Contours = bounds, contours
|
annotations[i].Bounds, annotations[i].Contours = bounds, contours
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clear(s.svgFonts)
|
|
||||||
for i, font := range resources.Fonts {
|
|
||||||
s.svgFonts[font.Name] = font.Data
|
|
||||||
resources.Fonts[i].Data = nil
|
|
||||||
}
|
|
||||||
return PageSVG{Index: index, Number: index + 1, ID: page.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links, Fonts: resources.Fonts, Images: resources.Images, Annotations: annotations}, nil
|
return PageSVG{Index: index, Number: index + 1, ID: page.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links, Fonts: resources.Fonts, Images: resources.Images, Annotations: annotations}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1282,26 +1293,21 @@ func (r *browserReaderAt) ReadAt(data []byte, offset int64) (int, error) {
|
|||||||
if offset >= r.size {
|
if offset >= r.size {
|
||||||
return n, io.EOF
|
return n, io.EOF
|
||||||
}
|
}
|
||||||
if time.Since(r.checked) >= 50*time.Millisecond {
|
r.mu.Lock()
|
||||||
|
check := r.checkpoint.Type() == platformTypeFunction && time.Since(r.checked) >= 50*time.Millisecond
|
||||||
|
if check {
|
||||||
|
r.checked = time.Now()
|
||||||
|
}
|
||||||
|
r.mu.Unlock()
|
||||||
|
if check {
|
||||||
if err := awaitExport(r.checkpoint); err != nil {
|
if err := awaitExport(r.checkpoint); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
r.checked = time.Now()
|
|
||||||
}
|
}
|
||||||
if len(r.data) == 0 || offset < r.offset || offset-r.offset >= int64(len(r.data)) {
|
copied, err := r.readChunk(data, offset)
|
||||||
r.offset = offset / (256 << 10) * (256 << 10)
|
if err != nil {
|
||||||
size := int(min(int64(256<<10), r.size-r.offset))
|
return n, err
|
||||||
if cap(r.data) < size {
|
|
||||||
r.data = make([]byte, size)
|
|
||||||
} else {
|
|
||||||
r.data = r.data[:size]
|
|
||||||
}
|
}
|
||||||
value := r.read.Invoke(float64(r.offset), size)
|
|
||||||
if platformCopyBytes(r.data, value) != size {
|
|
||||||
return n, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copied := copy(data, r.data[offset-r.offset:])
|
|
||||||
n += copied
|
n += copied
|
||||||
offset += int64(copied)
|
offset += int64(copied)
|
||||||
data = data[copied:]
|
data = data[copied:]
|
||||||
@@ -1309,6 +1315,31 @@ func (r *browserReaderAt) ReadAt(data []byte, offset int64) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readChunk 串行读取和复用缓存块,仅保留完整读取的内容
|
||||||
|
// 入参: data 目标缓冲, offset 文件内偏移
|
||||||
|
// 返回: int 复制字节数, error 读取错误
|
||||||
|
func (r *browserReaderAt) readChunk(data []byte, offset int64) (int, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if len(r.data) == 0 || offset < r.offset || offset-r.offset >= int64(len(r.data)) {
|
||||||
|
start := offset / (256 << 10) * (256 << 10)
|
||||||
|
size := int(min(int64(256<<10), r.size-start))
|
||||||
|
buffer := r.data
|
||||||
|
r.data = nil
|
||||||
|
if cap(buffer) < size {
|
||||||
|
buffer = make([]byte, size)
|
||||||
|
} else {
|
||||||
|
buffer = buffer[:size]
|
||||||
|
}
|
||||||
|
value := r.read.Invoke(float64(start), size)
|
||||||
|
if platformCopyBytes(buffer, value) != size {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
r.offset, r.data = start, buffer
|
||||||
|
}
|
||||||
|
return copy(data, r.data[offset-r.offset:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
// renderBackends 返回库提供的内置后端能力
|
// renderBackends 返回库提供的内置后端能力
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 后端能力, error 错误信息
|
// 返回: any 后端能力, error 错误信息
|
||||||
@@ -2440,7 +2471,7 @@ func (e *businessEngine) svgFontData(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderPage 渲染OFD页面
|
// renderPage 渲染OFD页面
|
||||||
// 入参: args 页面索引、后端组合、DPI和光栅显示开关
|
// 入参: args 页面索引、后端组合、DPI、光栅开关、已持有字体和图片、仅预览开关
|
||||||
// 返回: any 页面SVG结果, error 错误信息
|
// 返回: any 页面SVG结果, error 错误信息
|
||||||
func (e *businessEngine) renderPage(args []platformValue) (any, error) {
|
func (e *businessEngine) renderPage(args []platformValue) (any, error) {
|
||||||
if e.currentSession == nil {
|
if e.currentSession == nil {
|
||||||
@@ -2457,6 +2488,13 @@ func (e *businessEngine) renderPage(args []platformValue) (any, error) {
|
|||||||
dpi = args[2].Float()
|
dpi = args[2].Float()
|
||||||
}
|
}
|
||||||
raster := len(args) > 3 && args[3].Bool()
|
raster := len(args) > 3 && args[3].Bool()
|
||||||
|
preview := len(args) > 6 && args[6].Bool()
|
||||||
|
known := make(map[string]bool)
|
||||||
|
for i := 4; i < min(len(args), 6); i++ {
|
||||||
|
for j := 0; j < args[i].Length(); j++ {
|
||||||
|
known[args[i].Index(j).String()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if e.currentEditor != nil {
|
if e.currentEditor != nil {
|
||||||
page, err := e.currentEditor.Page(args[0].Int())
|
page, err := e.currentEditor.Page(args[0].Int())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2466,25 +2504,31 @@ func (e *businessEngine) renderPage(args []platformValue) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
page, err := e.currentSession.RenderPage(args[0].Int(), backend, dpi, raster)
|
page, err := e.currentSession.renderPage(args[0].Int(), backend, dpi, raster, preview, known)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.currentEditor != nil && !reflect.DeepEqual(e.currentEditor.Backends(), e.currentSession.Renderer.Backends()) {
|
if e.currentEditor != nil && !reflect.DeepEqual(e.currentEditor.Backends(), e.currentSession.Renderer.Backends()) {
|
||||||
e.currentEditor.SetRenderBackends(e.currentSession.Renderer.Backends())
|
e.currentEditor.SetRenderBackends(e.currentSession.Renderer.Backends())
|
||||||
}
|
}
|
||||||
text, err := e.currentSession.PageText(args[0].Int())
|
text := &ofdgo.PageText{}
|
||||||
|
if !preview {
|
||||||
|
text, err = e.currentSession.PageText(args[0].Int())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
textData, err := json.Marshal(text)
|
textData, err := json.Marshal(text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
objects, err := e.editorObjects(args[0].Int(), text)
|
var objects []any
|
||||||
|
if !preview {
|
||||||
|
objects, err = e.editorObjects(args[0].Int(), text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
links, err := e.linkResults(page.Links)
|
links, err := e.linkResults(page.Links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2492,12 +2536,16 @@ func (e *businessEngine) renderPage(args []platformValue) (any, error) {
|
|||||||
fonts := make([]any, len(page.Fonts))
|
fonts := make([]any, len(page.Fonts))
|
||||||
for i, font := range page.Fonts {
|
for i, font := range page.Fonts {
|
||||||
fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style}
|
fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style}
|
||||||
|
if len(args) > 4 && !known[font.Name] {
|
||||||
|
fonts[i].(map[string]any)["data"] = bytesToJS(e.currentSession.svgFonts[font.Name])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
annotations, err := json.Marshal(page.Annotations)
|
annotations, err := json.Marshal(page.Annotations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return successResult(map[string]any{
|
return successResult(map[string]any{
|
||||||
|
"previewOnly": preview,
|
||||||
"annotations": string(annotations),
|
"annotations": string(annotations),
|
||||||
"index": page.Index,
|
"index": page.Index,
|
||||||
"number": page.Number,
|
"number": page.Number,
|
||||||
@@ -5502,7 +5550,7 @@ func (e *businessEngine) clearImport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadImport 加载待导入文件的索引,null参数清除待处理文件
|
// loadImport 加载待导入文件的索引,null参数清除待处理文件
|
||||||
// 入参: args OFD字节数组或null
|
// 入参: args 文档输入或null及可选凭据
|
||||||
// 返回: any 文档列表、页数及签名提示, error 错误信息
|
// 返回: any 文档列表、页数及签名提示, error 错误信息
|
||||||
func (e *businessEngine) loadImport(args []platformValue) (any, error) {
|
func (e *businessEngine) loadImport(args []platformValue) (any, error) {
|
||||||
e.clearImport()
|
e.clearImport()
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
|
|||||||
font["data"] = worker.svgFonts[name]
|
font["data"] = worker.svgFonts[name]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.currentEditor != nil {
|
if e.currentEditor != nil && !(len(values) > 6 && values[6].Bool()) {
|
||||||
objects, textErr := e.renderObjects(operation, values[0].Int())
|
objects, textErr := e.renderObjects(operation, values[0].Int())
|
||||||
if textErr != nil {
|
if textErr != nil {
|
||||||
return nativeFailure(textErr)
|
return nativeFailure(textErr)
|
||||||
|
|||||||
@@ -221,8 +221,11 @@ func platformParseJSON(data []byte) map[string]any {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// inputFromValue 读取浏览器二进制源
|
// inputFromValue 读取浏览器随机输入或二进制数据
|
||||||
func (e *businessEngine) inputFromValue(value platformValue) (*ofdgo.Input, error) {
|
func (e *businessEngine) inputFromValue(value platformValue) (*ofdgo.Input, error) {
|
||||||
|
if value.Type() == platformTypeObject && !value.IsNull() && value.Get("read").Type() == platformTypeFunction {
|
||||||
|
return ofdgo.NewInput(&browserReaderAt{read: value.Get("read"), size: int64(value.Get("size").Float())}, int64(value.Get("size").Float()))
|
||||||
|
}
|
||||||
data, err := e.bytesFromJS(value)
|
data, err := e.bytesFromJS(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+5
-8
@@ -320,16 +320,13 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
|
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
|
||||||
img, ok := r.imageCache.get(resPath)
|
return r.imageCache.load(r.pageContext, resPath, func() (image.Image, int, error) {
|
||||||
if !ok {
|
img, err := r.readImageResource(resPath)
|
||||||
var err error
|
|
||||||
img, err = r.readImageResource(resPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
r.imageCache.put(resPath, img, baseimage.StorageCost(img, r.imageCache.limit))
|
return img, baseimage.StorageCost(img, r.imageCache.limit), nil
|
||||||
}
|
})
|
||||||
return img, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeImageResource 读取或复用图片资源,按输出需要解码像素
|
// decodeImageResource 读取或复用图片资源,按输出需要解码像素
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -29,6 +31,14 @@ type renderCache[K comparable, V any] struct {
|
|||||||
limit, used int
|
limit, used int
|
||||||
entries map[K]*list.Element
|
entries map[K]*list.Element
|
||||||
order list.List
|
order list.List
|
||||||
|
loading map[K]*renderCacheLoad[V]
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderCacheLoad 合并同一资源的并发加载,不持有已完成任务
|
||||||
|
type renderCacheLoad[V any] struct {
|
||||||
|
done chan struct{}
|
||||||
|
value V
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderCacheEntry 保存缓存值及内存成本
|
// renderCacheEntry 保存缓存值及内存成本
|
||||||
@@ -79,6 +89,61 @@ func (c *renderCache[K, V]) put(key K, value V, cost int) {
|
|||||||
c.used += cost
|
c.used += cost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load 复用缓存并合并并发请求,等待取消不影响正在读取的任务
|
||||||
|
// 入参: ctx 当前任务上下文, key 缓存键, read 加载资源及其内存成本
|
||||||
|
// 返回: V 缓存值, error 读取或取消错误
|
||||||
|
func (c *renderCache[K, V]) load(ctx context.Context, key K, read func() (V, int, error)) (V, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
var zero V
|
||||||
|
for {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
if item := c.entries[key]; item != nil {
|
||||||
|
c.order.MoveToFront(item)
|
||||||
|
value := item.Value.(renderCacheEntry[K, V]).value
|
||||||
|
c.mu.Unlock()
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
if pending := c.loading[key]; pending != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return zero, ctx.Err()
|
||||||
|
case <-pending.done:
|
||||||
|
if errors.Is(pending.err, context.Canceled) || errors.Is(pending.err, context.DeadlineExceeded) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return pending.value, pending.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.loading == nil {
|
||||||
|
c.loading = make(map[K]*renderCacheLoad[V])
|
||||||
|
}
|
||||||
|
pending := &renderCacheLoad[V]{done: make(chan struct{}), err: errors.New("resource loading interrupted")}
|
||||||
|
c.loading[key] = pending
|
||||||
|
c.mu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.loading, key)
|
||||||
|
close(pending.done)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}()
|
||||||
|
value, cost, err := read()
|
||||||
|
if err == nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
pending.value, pending.err = value, err
|
||||||
|
if err == nil {
|
||||||
|
c.put(key, value, cost)
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// rasterCommandCost 按保留容量估算中立指令成本,不含临时后端路径及借用图片
|
// rasterCommandCost 按保留容量估算中立指令成本,不含临时后端路径及借用图片
|
||||||
// 入参: command 绘制指令
|
// 入参: command 绘制指令
|
||||||
// 返回: int 字节成本
|
// 返回: int 字节成本
|
||||||
|
|||||||
+15
-1
@@ -50,6 +50,8 @@ type SVGResources struct {
|
|||||||
type PreviewOptions struct {
|
type PreviewOptions struct {
|
||||||
Raster bool
|
Raster bool
|
||||||
Objects bool
|
Objects bool
|
||||||
|
// KnownResources 标识调用方仍持有的资源,返回引用但不重复返回数据
|
||||||
|
KnownResources map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体
|
// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体
|
||||||
@@ -92,7 +94,19 @@ func (r *Renderer) renderSVG(page *PageContent, writer io.Writer, mode SVGMode)
|
|||||||
// RenderPreview 生成统一SVG容器及独立资源,原生程序与WebUI使用相同绘制入口
|
// RenderPreview 生成统一SVG容器及独立资源,原生程序与WebUI使用相同绘制入口
|
||||||
// 入参: page 页面内容, writer 输出流, options 显示方式
|
// 入参: page 页面内容, writer 输出流, options 显示方式
|
||||||
// 返回: SVGResources 需由调用方映射地址的资源, error 错误信息
|
// 返回: SVGResources 需由调用方映射地址的资源, error 错误信息
|
||||||
func (r *Renderer) RenderPreview(page *PageContent, writer io.Writer, options PreviewOptions) (SVGResources, error) {
|
func (r *Renderer) RenderPreview(page *PageContent, writer io.Writer, options PreviewOptions) (resources SVGResources, err error) {
|
||||||
|
defer func() {
|
||||||
|
for i := range resources.Fonts {
|
||||||
|
if options.KnownResources[resources.Fonts[i].Name] {
|
||||||
|
resources.Fonts[i].Data = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range resources.Images {
|
||||||
|
if options.KnownResources[resources.Images[i].Name] {
|
||||||
|
resources.Images[i].Data = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
if !options.Raster {
|
if !options.Raster {
|
||||||
if options.Objects {
|
if options.Objects {
|
||||||
return r.RenderToSVGWithObjects(page, writer)
|
return r.RenderToSVGWithObjects(page, writer)
|
||||||
|
|||||||
+12
-10
@@ -40,6 +40,7 @@ type PositionedText struct {
|
|||||||
|
|
||||||
// glyphOutlineKey 区分字体、字形与毫米字号
|
// glyphOutlineKey 区分字体、字形与毫米字号
|
||||||
type glyphOutlineKey struct {
|
type glyphOutlineKey struct {
|
||||||
|
digest [32]byte
|
||||||
font *PreparedFont
|
font *PreparedFont
|
||||||
glyph uint16
|
glyph uint16
|
||||||
size float64
|
size float64
|
||||||
@@ -279,21 +280,20 @@ func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error)
|
|||||||
prepared.digest = r.fontDigest(prepared.Data)
|
prepared.digest = r.fontDigest(prepared.Data)
|
||||||
}
|
}
|
||||||
key := prepared.digest
|
key := prepared.digest
|
||||||
if metrics, ok := r.fontMetrics.get(key); ok {
|
return r.fontMetrics.load(r.pageContext, key, func() (FontMetrics, int, error) {
|
||||||
return metrics, nil
|
|
||||||
}
|
|
||||||
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
|
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
|
||||||
if err != nil {
|
return metrics, len(prepared.Data)*2 + 256, err
|
||||||
return nil, err
|
})
|
||||||
}
|
|
||||||
r.fontMetrics.put(key, metrics, len(prepared.Data)*2+256)
|
|
||||||
return metrics, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// preparedOutlines 批量提取缓存未命中的字形,保留原始编号顺序,不触发塑形
|
// preparedOutlines 批量提取缓存未命中的字形,保留原始编号顺序,不触发塑形
|
||||||
// 入参: prepared 字体, metrics 度量, glyphs 字形编号, size 毫米字号
|
// 入参: prepared 字体, metrics 度量, glyphs 字形编号, size 毫米字号
|
||||||
// 返回: []GeometryPath 同序只读轮廓, error 能力或解析错误
|
// 返回: []GeometryPath 同序只读轮廓, error 能力或解析错误
|
||||||
func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, glyphs []uint16, size float64) ([]GeometryPath, error) {
|
func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics, glyphs []uint16, size float64) ([]GeometryPath, error) {
|
||||||
|
key := glyphOutlineKey{digest: prepared.digest, size: size}
|
||||||
|
if key.digest == ([32]byte{}) {
|
||||||
|
key.font = prepared
|
||||||
|
}
|
||||||
result := make([]GeometryPath, len(glyphs))
|
result := make([]GeometryPath, len(glyphs))
|
||||||
var indices []int
|
var indices []int
|
||||||
missing := make([]uint16, 0)
|
missing := make([]uint16, 0)
|
||||||
@@ -304,7 +304,8 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if path, ok := r.glyphOutlines.get(glyphOutlineKey{prepared, glyph, size}); ok {
|
key.glyph = glyph
|
||||||
|
if path, ok := r.glyphOutlines.get(key); ok {
|
||||||
result[i] = path
|
result[i] = path
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -360,7 +361,8 @@ func (r *Renderer) preparedOutlines(prepared *PreparedFont, metrics FontMetrics,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, path := range paths {
|
for i, path := range paths {
|
||||||
r.glyphOutlines.put(glyphOutlineKey{prepared, missing[i], size}, path, len(path)*96+128)
|
key.glyph = missing[i]
|
||||||
|
r.glyphOutlines.put(key, path, len(path)*96+128)
|
||||||
}
|
}
|
||||||
for i, index := range indices {
|
for i, index := range indices {
|
||||||
if index >= 0 {
|
if index >= 0 {
|
||||||
|
|||||||
Reference in new issue
Block a user