mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
1 parent
ea55e997ee
commit
83d936e9f8
8 files changed
+357
-41
No files matched your search
+52
-17
@@ -133,12 +133,14 @@ const state = {
|
|||||||
pageCache: new Map(),
|
pageCache: new Map(),
|
||||||
mediaPlayers: new Map(),
|
mediaPlayers: new Map(),
|
||||||
svgFonts: new Map(),
|
svgFonts: new Map(),
|
||||||
|
svgFontLoads: new Map(),
|
||||||
svgImages: new Map(),
|
svgImages: new Map(),
|
||||||
selectedPages: new Set(),
|
selectedPages: new Set(),
|
||||||
documentSelection: null,
|
documentSelection: null,
|
||||||
pageInFlight: new Map(),
|
pageInFlight: new Map(),
|
||||||
pageRenderQueue: [],
|
pageRenderQueue: [],
|
||||||
pageRenderRunning: false,
|
pageRenderRunning: false,
|
||||||
|
pageRenderWorkers: 0,
|
||||||
pageObserver: null,
|
pageObserver: null,
|
||||||
visiblePages: new Set(),
|
visiblePages: new Set(),
|
||||||
scrollFrame: 0,
|
scrollFrame: 0,
|
||||||
@@ -8969,6 +8971,11 @@ function resetPageFlow(keepCache = false) {
|
|||||||
|
|
||||||
function resetPageLoading() {
|
function resetPageLoading() {
|
||||||
releaseMediaPlayers();
|
releaseMediaPlayers();
|
||||||
|
if (nativeHost) {
|
||||||
|
for (const [id, request] of wasmRequests) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -9104,6 +9111,9 @@ function loadPageData(index, options = {}) {
|
|||||||
|
|
||||||
function schedulePageRender() {
|
function schedulePageRender() {
|
||||||
if (state.pageRenderRunning) {
|
if (state.pageRenderRunning) {
|
||||||
|
if (state.pageRenderWorkers) {
|
||||||
|
while (state.pageRenderQueue.length && state.pageRenderWorkers < (nativeHost?.parallelism || 1)) processPageRenderTasks();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.pageRenderRunning = true;
|
state.pageRenderRunning = true;
|
||||||
@@ -9111,8 +9121,13 @@ function schedulePageRender() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processPageRenderQueue() {
|
async function processPageRenderQueue() {
|
||||||
|
await nextFrame();
|
||||||
|
await Promise.all(Array.from({ length: nativeHost?.parallelism || 1 }, () => processPageRenderTasks()));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPageRenderTasks() {
|
||||||
|
state.pageRenderWorkers++;
|
||||||
try {
|
try {
|
||||||
await nextFrame();
|
|
||||||
while (state.pageRenderQueue.length) {
|
while (state.pageRenderQueue.length) {
|
||||||
let next = 0;
|
let next = 0;
|
||||||
for (let index = 1; index < state.pageRenderQueue.length; index++) {
|
for (let index = 1; index < state.pageRenderQueue.length; index++) {
|
||||||
@@ -9128,7 +9143,9 @@ async function processPageRenderQueue() {
|
|||||||
task.resolve(state.pageCache.get(task.index));
|
task.resolve(state.pageCache.get(task.index));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const page = await callWASM("ofdgoRenderPage", 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()]);
|
||||||
|
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);
|
||||||
continue;
|
continue;
|
||||||
@@ -9139,6 +9156,7 @@ async function processPageRenderQueue() {
|
|||||||
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
||||||
}
|
}
|
||||||
await loadSVGFonts(page.fonts, task.openSeq);
|
await loadSVGFonts(page.fonts, task.openSeq);
|
||||||
|
for (const font of page.fonts) delete font.data;
|
||||||
if (task.openSeq === state.openSeq && state.pageInFlight.get(task.key) === task) {
|
if (task.openSeq === state.openSeq && state.pageInFlight.get(task.key) === task) {
|
||||||
for (const image of page.images) {
|
for (const image of page.images) {
|
||||||
if (!state.svgImages.has(image.name)) {
|
if (!state.svgImages.has(image.name)) {
|
||||||
@@ -9172,8 +9190,11 @@ async function processPageRenderQueue() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.pageRenderRunning = false;
|
if (--state.pageRenderWorkers === 0) {
|
||||||
if (!state.pageInFlight.size) releasePageResources();
|
state.pageRenderRunning = false;
|
||||||
|
if (!state.pageInFlight.size) releasePageResources();
|
||||||
|
if (state.pageRenderQueue.length) schedulePageRender();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9196,8 +9217,9 @@ function trimPageCache() {
|
|||||||
for (const image of state.svgImages.values()) {
|
for (const image of state.svgImages.values()) {
|
||||||
bytes += image.size;
|
bytes += image.size;
|
||||||
}
|
}
|
||||||
|
const budget = nativeHost?.cacheBytes || PAGE_CACHE_BYTES;
|
||||||
for (const [index, page] of state.pageCache) {
|
for (const [index, page] of state.pageCache) {
|
||||||
if (state.pageCache.size <= PAGE_CACHE_LIMIT && bytes <= PAGE_CACHE_BYTES) {
|
if (state.pageCache.size <= PAGE_CACHE_LIMIT * Math.max(1, budget / PAGE_CACHE_BYTES) && bytes <= budget) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (protectedPages.has(index)) {
|
if (protectedPages.has(index)) {
|
||||||
@@ -9227,20 +9249,31 @@ async function loadSVGFonts(fonts, openSeq) {
|
|||||||
if (state.svgFonts.has(font.name)) {
|
if (state.svgFonts.has(font.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const data = await callWASM("ofdgoSVGFontData", font.name);
|
const key = `${openSeq}:${font.name}`;
|
||||||
if (openSeq !== state.openSeq) {
|
let loading = state.svgFontLoads.get(key);
|
||||||
return;
|
if (!loading) {
|
||||||
|
loading = loadSVGFont(font, openSeq);
|
||||||
|
state.svgFontLoads.set(key, loading);
|
||||||
}
|
}
|
||||||
const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style });
|
try { await loading; }
|
||||||
await face.load();
|
finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); }
|
||||||
if (openSeq !== state.openSeq) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.fonts.add(face);
|
|
||||||
state.svgFonts.set(font.name, face);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSVGFont(font, openSeq) {
|
||||||
|
const data = font.data ? { bytes: font.data } : await callWASM("ofdgoSVGFontData", font.name);
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style });
|
||||||
|
await face.load();
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.fonts.add(face);
|
||||||
|
state.svgFonts.set(font.name, face);
|
||||||
|
}
|
||||||
|
|
||||||
function comparePageRenderTask(a, b) {
|
function comparePageRenderTask(a, b) {
|
||||||
return a.priority - b.priority || Math.abs(a.index - state.pageIndex) - Math.abs(b.index - state.pageIndex);
|
return a.priority - b.priority || Math.abs(a.index - state.pageIndex) - Math.abs(b.index - state.pageIndex);
|
||||||
}
|
}
|
||||||
@@ -9853,7 +9886,9 @@ function markFlowPageError(index, err) {
|
|||||||
|
|
||||||
function queueNearbyPages(index, openSeq = state.openSeq) {
|
function queueNearbyPages(index, openSeq = state.openSeq) {
|
||||||
const start = pageGroupStart(index);
|
const start = pageGroupStart(index);
|
||||||
for (let i = Math.max(0, start - 1); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + 1); i += 1) {
|
const before = nativeHost ? state.pageLayout : 1;
|
||||||
|
const after = nativeHost ? Math.max(1, Math.min(nativeHost.parallelism, state.pageLayout * 2)) : 1;
|
||||||
|
for (let i = Math.max(0, start - before); i <= Math.min((state.doc?.pageCount || 1) - 1, start + state.pageLayout + after); i += 1) {
|
||||||
renderFlowPage(i, { openSeq, priority: 2 });
|
renderFlowPage(i, { openSeq, priority: 2 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12497,7 +12532,7 @@ async function callWASM(name, ...args) {
|
|||||||
el.cancelExportButton.disabled = false;
|
el.cancelExportButton.disabled = false;
|
||||||
}
|
}
|
||||||
if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id;
|
if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id;
|
||||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
wasmRequests.set(id, { name, resolve, reject, openSeq: state.openSeq });
|
||||||
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
|
if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0];
|
||||||
if (name === "ofdgoConvertPDF") {
|
if (name === "ofdgoConvertPDF") {
|
||||||
const request = wasmRequests.get(id);
|
const request = wasmRequests.get(id);
|
||||||
|
|||||||
+47
-20
@@ -87,6 +87,10 @@ class NativeHandle {
|
|||||||
class NativeHost {
|
class NativeHost {
|
||||||
constructor(bridge) {
|
constructor(bridge) {
|
||||||
this.bridge = bridge;
|
this.bridge = bridge;
|
||||||
|
this.parallelism = 1;
|
||||||
|
this.cacheBytes = 32 * 1024 * 1024;
|
||||||
|
this.reading = 0;
|
||||||
|
this.readQueue = [];
|
||||||
this.resources = typeof FinalizationRegistry === "function" ? new FinalizationRegistry(file => {
|
this.resources = typeof FinalizationRegistry === "function" ? new FinalizationRegistry(file => {
|
||||||
this.call("release", {file}).catch(() => {});
|
this.call("release", {file}).catch(() => {});
|
||||||
}) : null;
|
}) : null;
|
||||||
@@ -96,25 +100,40 @@ class NativeHost {
|
|||||||
return this.bridge.call(`ofdgo.host.${name}`, value, {signal});
|
return this.bridge.call(`ofdgo.host.${name}`, value, {signal});
|
||||||
}
|
}
|
||||||
|
|
||||||
async decode(value, name = "", key = "", signal) {
|
async readResource(file, signal) {
|
||||||
if (value?.nativeFile) {
|
if (this.reading >= this.parallelism) await new Promise(resolve => this.readQueue.push(resolve));
|
||||||
const file = new NativeFile(value);
|
else this.reading++;
|
||||||
if (key === "file" || name === "ofdgoConvertPDF" && key === "bytes") return file;
|
try {
|
||||||
try {
|
signal?.throwIfAborted();
|
||||||
const bytes = await file.arrayBuffer(signal);
|
return await file.arrayBuffer(signal);
|
||||||
return key === "blob" ? new Blob([bytes], {type:value.type}) : new Uint8Array(bytes);
|
} finally {
|
||||||
} finally {
|
try { await this.call("release", {file:file.nativeFile}); }
|
||||||
await this.call("release", {file:value.nativeFile});
|
finally {
|
||||||
|
const next = this.readQueue.shift();
|
||||||
|
if (next) next();
|
||||||
|
else this.reading--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
}
|
||||||
for (let i = 0; i < value.length; i++) value[i] = await this.decode(value[i], name, "", signal);
|
|
||||||
return value;
|
async decode(value, name = "", key = "", signal) {
|
||||||
}
|
const reads = [];
|
||||||
if (value && typeof value === "object") {
|
const visit = (item, field, replace) => {
|
||||||
for (const [field, item] of Object.entries(value)) value[field] = await this.decode(item, name, field, signal);
|
if (!item || typeof item !== "object" || item instanceof Blob || item instanceof ArrayBuffer || ArrayBuffer.isView(item)) return;
|
||||||
return value;
|
if (item.nativeFile) {
|
||||||
}
|
const file = new NativeFile(item);
|
||||||
|
if (field === "file" || name === "ofdgoConvertPDF" && field === "bytes") replace(file);
|
||||||
|
else reads.push(this.readResource(file, signal).then(bytes => replace(field === "blob" ? new Blob([bytes], {type:item.type}) : new Uint8Array(bytes))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [child, content] of Object.entries(item)) {
|
||||||
|
if (content && typeof content === "object") visit(content, Array.isArray(item) ? "" : child, result => item[child] = result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(value, key, result => value = result);
|
||||||
|
const results = await Promise.allSettled(reads);
|
||||||
|
const failed = results.find(result => result.status === "rejected");
|
||||||
|
if (failed) throw failed.reason;
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,14 +153,17 @@ class NativeHost {
|
|||||||
let closed = false;
|
let closed = false;
|
||||||
let unsubscribe;
|
let unsubscribe;
|
||||||
let pending = Promise.resolve();
|
let pending = Promise.resolve();
|
||||||
|
const readers = new Set();
|
||||||
const requests = new Map();
|
const requests = new Map();
|
||||||
const completions = new Map();
|
const completions = new Map();
|
||||||
const emit = data => {
|
const emit = data => {
|
||||||
if (data.type === "result") { completions.get(data.id)?.(data.result); return; }
|
if (data.type === "result") { completions.get(data.id)?.(data.result); return; }
|
||||||
if (!closed) worker.onmessage?.({data});
|
if (!closed) worker.onmessage?.({data});
|
||||||
};
|
};
|
||||||
const ready = host.call("engine.open", {}).then(({engine}) => {
|
const ready = host.call("engine.open", {}).then(({engine, parallelism, cacheBytes}) => {
|
||||||
if (closed) return engine;
|
if (closed) return engine;
|
||||||
|
host.parallelism = Math.max(1, parallelism || 1);
|
||||||
|
host.cacheBytes = cacheBytes || host.cacheBytes;
|
||||||
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
unsubscribe = host.bridge.on(`ofdgo.engine.${engine}`, emit);
|
||||||
emit({type:"ready"});
|
emit({type:"ready"});
|
||||||
return engine;
|
return engine;
|
||||||
@@ -158,7 +180,9 @@ class NativeHost {
|
|||||||
if (data.type === "font") return;
|
if (data.type === "font") return;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
requests.set(data.id, controller);
|
requests.set(data.id, controller);
|
||||||
const operation = pending.then(async () => {
|
const parallel = data.name === "ofdgoRenderPage";
|
||||||
|
const barrier = parallel ? pending : Promise.all([pending, ...readers]);
|
||||||
|
const operation = barrier.then(async () => {
|
||||||
const engine = await ready;
|
const engine = await ready;
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
controller.signal.throwIfAborted();
|
controller.signal.throwIfAborted();
|
||||||
@@ -189,7 +213,10 @@ class NativeHost {
|
|||||||
if (closed) { await host.release(result); return; }
|
if (closed) { await host.release(result); return; }
|
||||||
emit({id:data.id, ...decoded});
|
emit({id:data.id, ...decoded});
|
||||||
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => requests.delete(data.id));
|
||||||
if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
if (parallel) {
|
||||||
|
readers.add(operation);
|
||||||
|
operation.finally(() => readers.delete(operation));
|
||||||
|
} else if (data.name !== "ofdgoSaveSnapshot") pending = operation;
|
||||||
},
|
},
|
||||||
terminate() {
|
terminate() {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
|||||||
@@ -1012,7 +1012,11 @@ func (s *Session) pageContent(index int) (*ofdgo.PageContent, error) {
|
|||||||
return page, nil
|
return page, nil
|
||||||
}
|
}
|
||||||
clear(s.pageCache)
|
clear(s.pageCache)
|
||||||
page, err := s.Reader.PageContentByIndex(index)
|
ctx := s.Renderer.OutputContext
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
page, err := s.Reader.PageContentByIndexContext(ctx, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -63,12 +65,15 @@ type NativeOutputTarget struct {
|
|||||||
Base, Name string
|
Base, Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NativeEngine 保存独立会话,不同实例可并行,同一实例的回调不可重入Call或Close
|
// NativeEngine 保存独立会话,页面渲染可并行,修改等待已开始的只读任务完成
|
||||||
|
// 同一实例的回调不可重入Call或Close
|
||||||
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
|
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
|
||||||
type NativeEngine struct {
|
type NativeEngine struct {
|
||||||
businessEngine
|
businessEngine
|
||||||
// ConversionPool 协调不同会话的批量转换资源
|
// ConversionPool 协调不同会话的批量转换资源
|
||||||
ConversionPool *ofdgo.ConversionPool
|
ConversionPool *ofdgo.ConversionPool
|
||||||
|
// RenderParallelism 限制同时渲染的页面数,零值使用可用处理器数
|
||||||
|
RenderParallelism int
|
||||||
// Resolve 按授权标识提供资源读取源
|
// Resolve 按授权标识提供资源读取源
|
||||||
Resolve NativeResolver
|
Resolve NativeResolver
|
||||||
// LoadFont 按名称读取系统字体数据
|
// LoadFont 按名称读取系统字体数据
|
||||||
@@ -84,6 +89,11 @@ type NativeEngine struct {
|
|||||||
once sync.Once
|
once sync.Once
|
||||||
saves sync.WaitGroup
|
saves sync.WaitGroup
|
||||||
closed bool
|
closed bool
|
||||||
|
renderMu sync.Mutex
|
||||||
|
renderCount int
|
||||||
|
renderDone chan struct{}
|
||||||
|
renderSlots chan struct{}
|
||||||
|
renderIdle []*Session
|
||||||
}
|
}
|
||||||
|
|
||||||
// nativeReader 在会话当前操作上下文中分块读取,不捕获已结束请求的上下文
|
// nativeReader 在会话当前操作上下文中分块读取,不捕获已结束请求的上下文
|
||||||
@@ -139,12 +149,21 @@ func NewNativeEngine(resolve NativeResolver) *NativeEngine { return &NativeEngin
|
|||||||
func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit func(any)) (result map[string]any) {
|
func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit func(any)) (result map[string]any) {
|
||||||
e.once.Do(func() {
|
e.once.Do(func() {
|
||||||
e.gate = make(chan struct{}, 1)
|
e.gate = make(chan struct{}, 1)
|
||||||
|
e.renderSlots = make(chan struct{}, e.renderParallelism())
|
||||||
e.resolveInput = e.input
|
e.resolveInput = e.input
|
||||||
e.conversionPool = e.ConversionPool
|
e.conversionPool = e.ConversionPool
|
||||||
})
|
})
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
if name == "ofdgoRenderPage" {
|
||||||
|
select {
|
||||||
|
case e.renderSlots <- struct{}{}:
|
||||||
|
defer func() { <-e.renderSlots }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nativeFailure(ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case e.gate <- struct{}{}:
|
case e.gate <- struct{}{}:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -162,6 +181,84 @@ 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 {
|
||||||
|
defer func() {
|
||||||
|
if failure := recover(); failure != nil {
|
||||||
|
result = nativeFailure(fmt.Errorf("%v", failure))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
worker, err := e.renderSession(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nativeFailure(err)
|
||||||
|
}
|
||||||
|
operation := &businessEngine{currentSession: worker, currentEditor: e.currentEditor, ctx: ctx}
|
||||||
|
values := make([]platformValue, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
values[i] = platformValueOf(arg)
|
||||||
|
}
|
||||||
|
if len(values) > 0 && operation.currentEditor != nil {
|
||||||
|
pageErr := e.prepareRender(operation, values[0].Int())
|
||||||
|
if pageErr != nil {
|
||||||
|
return nativeFailure(pageErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operation.currentEditor = nil
|
||||||
|
e.renderMu.Lock()
|
||||||
|
if e.renderCount == 0 {
|
||||||
|
e.renderDone = make(chan struct{})
|
||||||
|
}
|
||||||
|
e.renderCount++
|
||||||
|
e.renderMu.Unlock()
|
||||||
|
<-e.gate
|
||||||
|
locked = false
|
||||||
|
defer func() {
|
||||||
|
worker.Renderer.OutputContext = context.Background()
|
||||||
|
e.renderMu.Lock()
|
||||||
|
e.currentSession.svgFonts = maps.Clone(worker.svgFonts)
|
||||||
|
e.renderIdle = append(e.renderIdle, worker)
|
||||||
|
e.renderCount--
|
||||||
|
if e.renderCount == 0 {
|
||||||
|
close(e.renderDone)
|
||||||
|
}
|
||||||
|
e.renderMu.Unlock()
|
||||||
|
}()
|
||||||
|
data, err := operation.safeCall(operation.renderPage, values)
|
||||||
|
if err != nil {
|
||||||
|
return nativeFailure(err)
|
||||||
|
}
|
||||||
|
result = nativePlain(data).(map[string]any)
|
||||||
|
if result["ok"] != true {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
page := result["data"].(map[string]any)
|
||||||
|
known := make(map[string]bool)
|
||||||
|
if len(args) > 4 {
|
||||||
|
for i := 0; i < values[4].Length(); i++ {
|
||||||
|
known[values[4].Index(i).String()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range page["fonts"].([]any) {
|
||||||
|
font := value.(map[string]any)
|
||||||
|
if name := font["name"].(string); !known[name] {
|
||||||
|
font["data"] = worker.svgFonts[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.currentEditor != nil {
|
||||||
|
objects, textErr := e.renderObjects(operation, values[0].Int())
|
||||||
|
if textErr != nil {
|
||||||
|
return nativeFailure(textErr)
|
||||||
|
}
|
||||||
|
page["objects"] = nativePlain(objects)
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nativeFailure(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if err := e.waitRenders(ctx); err != nil {
|
||||||
|
return nativeFailure(err)
|
||||||
|
}
|
||||||
|
e.renderIdle = nil
|
||||||
if name == "ofdgoSaveSnapshot" {
|
if name == "ofdgoSaveSnapshot" {
|
||||||
snapshot := e.saveSnapshot
|
snapshot := e.saveSnapshot
|
||||||
e.saveSnapshot = nil
|
e.saveSnapshot = nil
|
||||||
@@ -236,10 +333,16 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
|
|||||||
|
|
||||||
// Close 释放会话,不关闭宿主持有的ReaderAt资源
|
// Close 释放会话,不关闭宿主持有的ReaderAt资源
|
||||||
func (e *NativeEngine) Close() error {
|
func (e *NativeEngine) Close() error {
|
||||||
e.once.Do(func() { e.gate = make(chan struct{}, 1); e.resolveInput = e.input })
|
e.once.Do(func() {
|
||||||
|
e.gate = make(chan struct{}, 1)
|
||||||
|
e.renderSlots = make(chan struct{}, e.renderParallelism())
|
||||||
|
e.resolveInput = e.input
|
||||||
|
})
|
||||||
e.gate <- struct{}{}
|
e.gate <- struct{}{}
|
||||||
defer func() { <-e.gate }()
|
defer func() { <-e.gate }()
|
||||||
e.closed = true
|
e.closed = true
|
||||||
|
_ = e.waitRenders(context.Background())
|
||||||
|
e.renderIdle = nil
|
||||||
e.saveSnapshot = nil
|
e.saveSnapshot = nil
|
||||||
e.saves.Wait()
|
e.saves.Wait()
|
||||||
var err error
|
var err error
|
||||||
@@ -253,6 +356,85 @@ func (e *NativeEngine) Close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitRenders 等待已开始的页面任务,取消等待不修改当前会话
|
||||||
|
// 入参: ctx 等待上下文
|
||||||
|
// 返回: error 取消错误
|
||||||
|
func (e *NativeEngine) waitRenders(ctx context.Context) error {
|
||||||
|
e.renderMu.Lock()
|
||||||
|
done, count := e.renderDone, e.renderCount
|
||||||
|
e.renderMu.Unlock()
|
||||||
|
if count == 0 {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderParallelism 返回首次调用时采用的页面并行数
|
||||||
|
// 返回: int 同时运行的页面任务数
|
||||||
|
func (e *NativeEngine) renderParallelism() int {
|
||||||
|
if e.RenderParallelism > 0 {
|
||||||
|
return e.RenderParallelism
|
||||||
|
}
|
||||||
|
return runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderSession 复用独立只读会话,调用时已持有引擎门控
|
||||||
|
// 入参: ctx 渲染上下文
|
||||||
|
// 返回: *Session 渲染会话, error 准备错误
|
||||||
|
func (e *NativeEngine) renderSession(ctx context.Context) (*Session, error) {
|
||||||
|
e.renderMu.Lock()
|
||||||
|
defer e.renderMu.Unlock()
|
||||||
|
if count := len(e.renderIdle); count > 0 {
|
||||||
|
worker := e.renderIdle[count-1]
|
||||||
|
e.renderIdle = e.renderIdle[:count-1]
|
||||||
|
worker.Renderer.OutputContext = ctx
|
||||||
|
return worker, nil
|
||||||
|
}
|
||||||
|
source := e.currentSession
|
||||||
|
renderer, err := source.Renderer.Fork(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Session{Reader: renderer.Reader, Renderer: renderer, doc: source.doc, fontFS: source.fontFS,
|
||||||
|
pageCache: make(map[int]*ofdgo.PageContent), boxCache: append([]pageBoxInfo(nil), source.boxCache...),
|
||||||
|
textCache: make(map[int]*ofdgo.PageText), svgFonts: make(map[string][]byte), editing: source.editing}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareRender 同步当前编辑页面的图元标识,避免并行访问编辑器缓存
|
||||||
|
// 入参: operation 只读任务, index 页面索引
|
||||||
|
// 返回: error 页面或取消错误
|
||||||
|
func (e *NativeEngine) prepareRender(operation *businessEngine, index int) error {
|
||||||
|
e.renderMu.Lock()
|
||||||
|
defer e.renderMu.Unlock()
|
||||||
|
page, err := operation.currentEditor.Page(index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return operation.syncEditorPageIDs(index, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderObjects 读取编辑图元信息,渲染状态由当前任务独立持有
|
||||||
|
// 入参: operation 只读任务, index 页面索引
|
||||||
|
// 返回: []any 图元信息, error 提取错误
|
||||||
|
func (e *NativeEngine) renderObjects(operation *businessEngine, index int) ([]any, error) {
|
||||||
|
e.renderMu.Lock()
|
||||||
|
defer e.renderMu.Unlock()
|
||||||
|
operation.currentEditor = e.currentEditor
|
||||||
|
if !reflect.DeepEqual(e.currentEditor.Backends(), operation.currentSession.Renderer.Backends()) {
|
||||||
|
e.currentEditor.SetRenderBackends(operation.currentSession.Renderer.Backends())
|
||||||
|
}
|
||||||
|
text, err := operation.currentSession.PageText(index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return operation.editorObjects(index, text)
|
||||||
|
}
|
||||||
|
|
||||||
// input 读取授权源并以宿主报告的大小为准
|
// input 读取授权源并以宿主报告的大小为准
|
||||||
func (e *NativeEngine) input(ctx context.Context, value any) (io.ReaderAt, int64, error) {
|
func (e *NativeEngine) input(ctx context.Context, value any) (io.ReaderAt, int64, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -267,6 +268,13 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
|||||||
switch name {
|
switch name {
|
||||||
case "engine.open":
|
case "engine.open":
|
||||||
engine := webui.NewNativeEngine(h.files.resolve)
|
engine := webui.NewNativeEngine(h.files.resolve)
|
||||||
|
parallelism, cacheBytes := runtime.GOMAXPROCS(0), int64(128<<20)
|
||||||
|
if memory := conversionMemory; memory != nil {
|
||||||
|
available := memory()
|
||||||
|
parallelism = max(1, min(parallelism, int(available/(256<<20))))
|
||||||
|
cacheBytes = max(32<<20, min(256<<20, available/16))
|
||||||
|
}
|
||||||
|
engine.RenderParallelism = parallelism
|
||||||
engine.ConversionPool = h.conversionPool
|
engine.ConversionPool = h.conversionPool
|
||||||
engine.OpenWriter = h.openWriter
|
engine.OpenWriter = h.openWriter
|
||||||
engine.ReserveOutput = h.reserveOutput
|
engine.ReserveOutput = h.reserveOutput
|
||||||
@@ -280,7 +288,7 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
|
|||||||
}
|
}
|
||||||
h.engineStops[id] = context.AfterFunc(guigo.PageContext(ctx), func() { _ = h.closeEngine(id) })
|
h.engineStops[id] = context.AfterFunc(guigo.PageContext(ctx), func() { _ = h.closeEngine(id) })
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
return guigo.Result{Value: map[string]any{"engine": id}, Discard: func() { _ = h.closeEngine(id) }}, nil
|
return guigo.Result{Value: map[string]any{"engine": id, "parallelism": parallelism, "cacheBytes": cacheBytes}, Discard: func() { _ = h.closeEngine(id) }}, nil
|
||||||
case "batch":
|
case "batch":
|
||||||
return map[string]any{"parallelism": h.conversionPool.Parallelism()}, nil
|
return map[string]any{"parallelism": h.conversionPool.Parallelism()}, nil
|
||||||
case "engine.close":
|
case "engine.close":
|
||||||
|
|||||||
@@ -95,6 +95,33 @@ func (r *Reader) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fork 创建保留当前文档及历史版本的独立只读视图,不复制包内文件数据
|
||||||
|
// 创建过程需与原实例串行,返回后可并行读取,公开结构及底层输入不得并发修改
|
||||||
|
// 返回值不持有输入所有权,全部视图使用结束前不得关闭或替换原输入
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Reader 独立阅读器, error 准备或取消错误
|
||||||
|
func (r *Reader) Fork(ctx context.Context) (*Reader, error) {
|
||||||
|
if _, err := r.DocContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next := *r
|
||||||
|
next.Closer = nil
|
||||||
|
next.resourcesRead = maps.Clone(r.resourcesRead)
|
||||||
|
next.resourceFiles = maps.Clone(r.resourceFiles)
|
||||||
|
next.ResMap = maps.Clone(r.ResMap)
|
||||||
|
next.mediaCache = maps.Clone(r.mediaCache)
|
||||||
|
next.fontCache = maps.Clone(r.fontCache)
|
||||||
|
next.fontFaces = maps.Clone(r.fontFaces)
|
||||||
|
next.colorSpaceCache = maps.Clone(r.colorSpaceCache)
|
||||||
|
next.drawParamCache = maps.Clone(r.drawParamCache)
|
||||||
|
next.compositeGraphicUnitCache = maps.Clone(r.compositeGraphicUnitCache)
|
||||||
|
next.pageHeaderCache = maps.Clone(r.pageHeaderCache)
|
||||||
|
next.Annots = maps.Clone(r.Annots)
|
||||||
|
next.Stamps = maps.Clone(r.Stamps)
|
||||||
|
next.annotationFiles = maps.Clone(r.annotationFiles)
|
||||||
|
return &next, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// DocumentCount 获取包内文档数量
|
// DocumentCount 获取包内文档数量
|
||||||
// 返回: int 文档数量
|
// 返回: int 文档数量
|
||||||
func (r *Reader) DocumentCount() int {
|
func (r *Reader) DocumentCount() int {
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ import (
|
|||||||
"container/list"
|
"container/list"
|
||||||
"image"
|
"image"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
|
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
|
||||||
type renderCache[K comparable, V any] struct {
|
type renderCache[K comparable, V any] struct {
|
||||||
|
mu sync.Mutex
|
||||||
limit, used int
|
limit, used int
|
||||||
entries map[K]*list.Element
|
entries map[K]*list.Element
|
||||||
order list.List
|
order list.List
|
||||||
@@ -40,6 +42,8 @@ type renderCacheEntry[K comparable, V any] struct {
|
|||||||
// 入参: key 缓存键
|
// 入参: key 缓存键
|
||||||
// 返回: V 缓存值, bool 是否命中
|
// 返回: V 缓存值, bool 是否命中
|
||||||
func (c *renderCache[K, V]) get(key K) (V, bool) {
|
func (c *renderCache[K, V]) get(key K) (V, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
if item := c.entries[key]; item != nil {
|
if item := c.entries[key]; item != nil {
|
||||||
c.order.MoveToFront(item)
|
c.order.MoveToFront(item)
|
||||||
return item.Value.(renderCacheEntry[K, V]).value, true
|
return item.Value.(renderCacheEntry[K, V]).value, true
|
||||||
@@ -51,6 +55,8 @@ func (c *renderCache[K, V]) get(key K) (V, bool) {
|
|||||||
// put 缓存预算内的派生数据,超大单项不进入缓存
|
// put 缓存预算内的派生数据,超大单项不进入缓存
|
||||||
// 入参: key 缓存键, value 缓存值, cost 字节成本
|
// 入参: key 缓存键, value 缓存值, cost 字节成本
|
||||||
func (c *renderCache[K, V]) put(key K, value V, cost int) {
|
func (c *renderCache[K, V]) put(key K, value V, cost int) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
if item := c.entries[key]; item != nil {
|
if item := c.entries[key]; item != nil {
|
||||||
c.used -= item.Value.(renderCacheEntry[K, V]).cost
|
c.used -= item.Value.(renderCacheEntry[K, V]).cost
|
||||||
c.order.Remove(item)
|
c.order.Remove(item)
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"image"
|
"image"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Renderer 渲染器实现
|
// Renderer 渲染器实现
|
||||||
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
||||||
|
// Fork创建独立任务实例,派生实例之间可并行渲染
|
||||||
// 外部字体缺失且无可用回退时不绘制对应文字,其余内容继续;字体状态可通过FontInfos查询
|
// 外部字体缺失且无可用回退时不绘制对应文字,其余内容继续;字体状态可通过FontInfos查询
|
||||||
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
||||||
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
||||||
@@ -65,6 +67,31 @@ type Renderer struct {
|
|||||||
// RendererOption 渲染器配置选项
|
// RendererOption 渲染器配置选项
|
||||||
type RendererOption func(*Renderer)
|
type RendererOption func(*Renderer)
|
||||||
|
|
||||||
|
// Fork 创建独立渲染实例,复用只读资源及线程安全的派生数据缓存
|
||||||
|
// 创建过程需与原实例串行,输入生命周期遵循Reader.Fork,外部后端及字体文件系统需支持并行读取
|
||||||
|
// 页面文字及导出进度回调不继承,取消上下文使用ctx
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Renderer 独立渲染器, error 准备或取消错误
|
||||||
|
func (r *Renderer) Fork(ctx context.Context) (*Renderer, error) {
|
||||||
|
reader, err := r.Reader.Fork(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next := NewRenderer(reader, WithDPI(r.DPI), WithAnnotations(r.RenderAnnotations), WithRenderBackends(r.backends), WithFontDirs(r.fontDirs...), WithFontFS(r.fontFS...))
|
||||||
|
next.Compression = r.Compression
|
||||||
|
next.DrawParams = maps.Clone(r.DrawParams)
|
||||||
|
next.CompositeGraphicUnits = maps.Clone(r.CompositeGraphicUnits)
|
||||||
|
next.OutputContext = ctx
|
||||||
|
next.TransparentBackground = r.TransparentBackground
|
||||||
|
next.decodeImages = r.decodeImages
|
||||||
|
next.imageCache = r.imageCache
|
||||||
|
next.fontDigests = r.fontDigests
|
||||||
|
next.fontPreparations = r.fontPreparations
|
||||||
|
next.fontMetrics = r.fontMetrics
|
||||||
|
next.glyphOutlines = r.glyphOutlines
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
// deviceLineWidth 按设备分辨率计算描边宽度,零线宽为一像素,正线宽至少为两像素
|
// deviceLineWidth 按设备分辨率计算描边宽度,零线宽为一像素,正线宽至少为两像素
|
||||||
// 入参: width 页面空间线宽,单位为毫米
|
// 入参: width 页面空间线宽,单位为毫米
|
||||||
// 返回: float64 设备描边宽度,单位为毫米
|
// 返回: float64 设备描边宽度,单位为毫米
|
||||||
|
|||||||
Reference in new issue
Block a user