mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
dfd4f733f9
commit
7bc31b6b84
6 files changed
+293
-97
No files matched your search
+34
-7
@@ -2961,6 +2961,8 @@ async function mountDocumentTab(tab) {
|
|||||||
|
|
||||||
async function releaseDocumentTab(tab) {
|
async function releaseDocumentTab(tab) {
|
||||||
if (tab.closed) return;
|
if (tab.closed) return;
|
||||||
|
const clipboard = workspace.objectClipboard;
|
||||||
|
if (clipboard?.tab === tab && !await prepareObjectClipboard(clipboard, tab) && workspace.objectClipboard === clipboard) workspace.objectClipboard = null;
|
||||||
tab.closed = true;
|
tab.closed = true;
|
||||||
tab.print?.destroy();
|
tab.print?.destroy();
|
||||||
tab.print = null;
|
tab.print = null;
|
||||||
@@ -6574,7 +6576,9 @@ function completePDF(tab) {
|
|||||||
throw new DOMException("文档已关闭", "AbortError");
|
throw new DOMException("文档已关闭", "AbortError");
|
||||||
})();
|
})();
|
||||||
task.promise.catch(err => {
|
task.promise.catch(err => {
|
||||||
if (current() && err.name !== "AbortError") {
|
if (!current()) return;
|
||||||
|
tab.pdfTask = null;
|
||||||
|
if (err.name !== "AbortError") {
|
||||||
tab.status = err.message;
|
tab.status = err.message;
|
||||||
if (isForegroundDocument(tab)) showError(err, false);
|
if (isForegroundDocument(tab)) showError(err, false);
|
||||||
}
|
}
|
||||||
@@ -10490,7 +10494,7 @@ function copyEditorSelection(event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const index = items[0].index, ids = items.map(item => item.id);
|
const index = items[0].index, ids = items.map(item => item.id);
|
||||||
const clipboard = { token: crypto.randomUUID(), source: `${tab.id}:${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`, page: state.doc.pages[index].id, bounds: selectionBounds(items), x: 0, y: 0, cut };
|
const clipboard = { tab, token: crypto.randomUUID(), source: `${tab.id}:${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`, page: state.doc.pages[index].id, bounds: selectionBounds(items), x: 0, y: 0, cut };
|
||||||
if (canvasEditor.nudge) {
|
if (canvasEditor.nudge) {
|
||||||
clipboard.bounds.x += canvasEditor.nudge.x;
|
clipboard.bounds.x += canvasEditor.nudge.x;
|
||||||
clipboard.bounds.y += canvasEditor.nudge.y;
|
clipboard.bounds.y += canvasEditor.nudge.y;
|
||||||
@@ -10499,11 +10503,14 @@ function copyEditorSelection(event) {
|
|||||||
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
event.clipboardData.setData("text/plain", items.map(item => item.text || "").filter(Boolean).join("\n"));
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
state.objectClipboardToken = clipboard.token;
|
state.objectClipboardToken = clipboard.token;
|
||||||
|
const previous = workspace.objectClipboard;
|
||||||
workspace.objectClipboard = clipboard;
|
workspace.objectClipboard = clipboard;
|
||||||
|
if (previous?.tab) previous.ready.then(() => {
|
||||||
|
if (previous.tab && !previous.tab.closed) return callTabWASM(previous.tab, "ofdgoDiscardObjects", previous.token);
|
||||||
|
}).catch(() => {});
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
const openSeq = state.openSeq, revision = state.editorInfo.revision;
|
||||||
return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async result => {
|
return callTabWASM(tab, "ofdgoCaptureObjects", index, ids, clipboard.token).then(async () => {
|
||||||
clipboard.data = result.bytes;
|
|
||||||
clipboard.cut = false;
|
clipboard.cut = false;
|
||||||
if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex
|
if (cut && isForegroundDocument(tab) && openSeq === state.openSeq && revision === state.editorInfo?.revision && index === state.pageIndex
|
||||||
&& state.objectClipboardToken === clipboard.token && workspace.objectClipboard === clipboard
|
&& state.objectClipboardToken === clipboard.token && workspace.objectClipboard === clipboard
|
||||||
@@ -10520,6 +10527,24 @@ function copyEditorSelection(event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareObjectClipboard(clipboard, owner = documentTab()) {
|
||||||
|
if (!await clipboard.ready) return false;
|
||||||
|
if (clipboard.data) return true;
|
||||||
|
if (!clipboard.portable) {
|
||||||
|
const tab = clipboard.tab;
|
||||||
|
clipboard.portable = callTabWASM(tab, "ofdgoExportObjects", clipboard.token).then(result => {
|
||||||
|
clipboard.data = result.bytes;
|
||||||
|
clipboard.tab = null;
|
||||||
|
return true;
|
||||||
|
}, err => {
|
||||||
|
clipboard.portable = null;
|
||||||
|
if (workspace.objectClipboard === clipboard && isForegroundDocument(owner)) showError(err, false);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return clipboard.portable;
|
||||||
|
}
|
||||||
|
|
||||||
function visiblePageBounds(index) {
|
function visiblePageBounds(index) {
|
||||||
const page = state.doc.pages[index], surface = pageView.pageShell(index)?.querySelector(".page-surface");
|
const page = state.doc.pages[index], surface = pageView.pageShell(index)?.querySelector(".page-surface");
|
||||||
const full = { x: 0, y: 0, width: page.width, height: page.height };
|
const full = { x: 0, y: 0, width: page.width, height: page.height };
|
||||||
@@ -10548,8 +10573,10 @@ async function pasteEditorContent(event) {
|
|||||||
setStatus("复制内容已失效,请重新复制");
|
setStatus("复制内容已失效,请重新复制");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq, identity = `${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}`;
|
||||||
if (!await clipboard.ready || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || workspace.objectClipboard !== clipboard) return;
|
const local = state.objectClipboardToken === clipboard.token;
|
||||||
|
if (!await (local ? clipboard.ready : prepareObjectClipboard(clipboard, tab)) || !isForegroundDocument(tab) || openSeq !== state.openSeq || index !== state.pageIndex || workspace.objectClipboard !== clipboard) return;
|
||||||
|
if (identity !== `${state.wasmSeq}:${state.doc.documentIndex || 0}:${state.doc.documentVersion || ""}` || local && state.objectClipboardToken !== clipboard.token) return;
|
||||||
if (!state.editing || formDialogOpen() || document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || canvasEditor.nudge || canvasEditor.nudgeCommit) return;
|
if (!state.editing || formDialogOpen() || document.body.hasAttribute("data-busy") || canvasEditor.input || canvasEditor.crop || canvasEditor.drag || canvasEditor.nudge || canvasEditor.nudgeCommit) return;
|
||||||
if (!pageCan("insert")) return;
|
if (!pageCan("insert")) return;
|
||||||
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
const visible = visiblePageBounds(index), bounds = clipboard.bounds;
|
||||||
@@ -10562,7 +10589,7 @@ async function pasteEditorContent(event) {
|
|||||||
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
offset[axis] = Math.max(visible[axis] + margin, Math.min(position, visible[axis] + visible[size] - margin - bounds[size])) - bounds[axis];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (await changeDocument(state.objectClipboardToken === clipboard.token ? "ofdgoPasteObjects" : "ofdgoImportObjects", null, index, state.objectClipboardToken === clipboard.token ? token : clipboard.data, offset.x, offset.y)) {
|
if (await changeDocument(local ? "ofdgoPasteObjects" : "ofdgoImportObjects", null, index, local ? token : clipboard.data, offset.x, offset.y)) {
|
||||||
clipboard.target = target;
|
clipboard.target = target;
|
||||||
clipboard.targetPage = page.id;
|
clipboard.targetPage = page.id;
|
||||||
clipboard.x = offset.x;
|
clipboard.x = offset.x;
|
||||||
|
|||||||
+60
-25
@@ -395,28 +395,47 @@ function resizeCorner(node, event) {
|
|||||||
return corner;
|
return corner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignmentSnap(box, targets, tolerance) {
|
export class AlignmentIndex {
|
||||||
const lines = [];
|
constructor(targets) {
|
||||||
const offset = { x: 0, y: 0 };
|
for (const [axis, size] of [["x", "width"], ["y", "height"]]) {
|
||||||
for (const [axis, size, cross, span] of [["x", "width", "y", "height"], ["y", "height", "x", "width"]]) {
|
this[axis] = targets.flatMap((target, index) => [0, 0.5, 1].map((anchor, order) => ({
|
||||||
let distance = Infinity, line;
|
position: target[axis] + target[size] * anchor, target, order: index * 3 + order,
|
||||||
for (const target of targets) {
|
}))).sort((a, b) => a.position - b.position || a.order - b.order);
|
||||||
for (const anchor of [0, 0.5, 1]) {
|
}
|
||||||
const position = target[axis] + target[size] * anchor;
|
}
|
||||||
for (const edge of [0, 0.5, 1]) {
|
|
||||||
const delta = position - box[axis] - box[size] * edge;
|
snap(box, tolerance) {
|
||||||
if (Math.abs(delta) > tolerance[axis] || Math.abs(delta) >= distance) continue;
|
const lines = [], offset = { x: 0, y: 0 };
|
||||||
distance = Math.abs(delta);
|
for (const [axis, size, cross, span] of [["x", "width", "y", "height"], ["y", "height", "x", "width"]]) {
|
||||||
|
const anchors = this[axis];
|
||||||
|
let best, distance = Infinity, rank = Infinity;
|
||||||
|
for (let edge = 0; edge < 3; edge++) {
|
||||||
|
const value = box[axis] + box[size] * edge / 2;
|
||||||
|
let low = 0, high = anchors.length;
|
||||||
|
while (low < high) {
|
||||||
|
const middle = (low + high) >> 1;
|
||||||
|
if (anchors[middle].position < value - tolerance[axis]) low = middle + 1;
|
||||||
|
else high = middle;
|
||||||
|
}
|
||||||
|
for (let i = low; i < anchors.length && anchors[i].position <= value + tolerance[axis]; i++) {
|
||||||
|
const anchor = anchors[i], delta = anchor.position - box[axis] - box[size] * edge / 2;
|
||||||
|
const next = Math.abs(delta), order = anchor.order * 3 + edge;
|
||||||
|
if (next > tolerance[axis] || next > distance || next === distance && order >= rank) continue;
|
||||||
|
best = anchor;
|
||||||
|
distance = next;
|
||||||
|
rank = order;
|
||||||
offset[axis] = delta;
|
offset[axis] = delta;
|
||||||
const from = Math.min(box[cross], target[cross]);
|
|
||||||
const to = Math.max(box[cross] + box[span], target[cross] + target[span]);
|
|
||||||
line = axis === "x" ? `M${position} ${from}V${to}` : `M${from} ${position}H${to}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (best) {
|
||||||
|
const { position, target } = best;
|
||||||
|
const from = Math.min(box[cross], target[cross]);
|
||||||
|
const to = Math.max(box[cross] + box[span], target[cross] + target[span]);
|
||||||
|
lines.push(axis === "x" ? `M${position} ${from}V${to}` : `M${from} ${position}H${to}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (line) lines.push(line);
|
return { ...offset, path: lines.join("") };
|
||||||
}
|
}
|
||||||
return { ...offset, path: lines.join("") };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformedBox(box, matrix) {
|
function transformedBox(box, matrix) {
|
||||||
@@ -548,6 +567,23 @@ export class ObjectIndex {
|
|||||||
this.root = build([...items]);
|
this.root = build([...items]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryBox(box) {
|
||||||
|
const result = [];
|
||||||
|
const intersects = node => node.x <= box.x + box.width && node.x + node.width >= box.x
|
||||||
|
&& node.y <= box.y + box.height && node.y + node.height >= box.y;
|
||||||
|
const visit = node => {
|
||||||
|
if (!node || !intersects(node)) return;
|
||||||
|
if (node.items) {
|
||||||
|
for (const item of node.items) {
|
||||||
|
if (!item.parent && item.x >= box.x && item.x + item.width <= box.x + box.width
|
||||||
|
&& item.y >= box.y && item.y + item.height <= box.y + box.height) result.push(item);
|
||||||
|
}
|
||||||
|
} else { visit(node.left); visit(node.right); }
|
||||||
|
};
|
||||||
|
visit(this.root);
|
||||||
|
return result.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|
||||||
query(point, tolerance = 0) {
|
query(point, tolerance = 0) {
|
||||||
const result = [];
|
const result = [];
|
||||||
const contains = box => point.x >= box.x - tolerance && point.x <= box.x + box.width + tolerance
|
const contains = box => point.x >= box.x - tolerance && point.x <= box.x + box.width + tolerance
|
||||||
@@ -950,9 +986,10 @@ export class CanvasEditor {
|
|||||||
};
|
};
|
||||||
if (!this.drag.corner) {
|
if (!this.drag.corner) {
|
||||||
const selected = this.items();
|
const selected = this.items();
|
||||||
this.drag.targets = [...item.surface.querySelectorAll(".edit-object")]
|
const targets = [...item.surface.querySelectorAll(".edit-object")]
|
||||||
.map(node => this.nodes.get(node)).filter(member => !selected.some(item => item === member || item.id.startsWith(member.id + "/") || member.id.startsWith(item.id + "/")));
|
.map(node => this.nodes.get(node)).filter(member => !selected.some(item => item === member || item.id.startsWith(member.id + "/") || member.id.startsWith(item.id + "/")));
|
||||||
this.drag.targets.push({ x: 0, y: 0, ...item.page });
|
targets.push({ x: 0, y: 0, ...item.page });
|
||||||
|
this.drag.alignment = new AlignmentIndex(targets);
|
||||||
}
|
}
|
||||||
if (lineShape(item.shape) && this.drag.corner) {
|
if (lineShape(item.shape) && this.drag.corner) {
|
||||||
Object.assign(this.drag, this.createPreview(item.surface, item.page, item.shape, {
|
Object.assign(this.drag, this.createPreview(item.surface, item.page, item.shape, {
|
||||||
@@ -1133,7 +1170,7 @@ export class CanvasEditor {
|
|||||||
|
|
||||||
snap(drag, bypass = false) {
|
snap(drag, bypass = false) {
|
||||||
const { item, rect, rotation, change } = drag;
|
const { item, rect, rotation, change } = drag;
|
||||||
const { x, y, path } = bypass ? { x: 0, y: 0, path: "" } : alignmentSnap({ ...item, x: item.x + change.x, y: item.y + change.y }, drag.targets, {
|
const { x, y, path } = bypass ? { x: 0, y: 0, path: "" } : drag.alignment.snap({ ...item, x: item.x + change.x, y: item.y + change.y }, {
|
||||||
x: drag.axis === "y" ? 0 : 4 * item.page.width / (rotation % 180 ? rect.height : rect.width),
|
x: drag.axis === "y" ? 0 : 4 * item.page.width / (rotation % 180 ? rect.height : rect.width),
|
||||||
y: drag.axis === "x" ? 0 : 4 * item.page.height / (rotation % 180 ? rect.width : rect.height),
|
y: drag.axis === "x" ? 0 : 4 * item.page.height / (rotation % 180 ? rect.width : rect.height),
|
||||||
});
|
});
|
||||||
@@ -1362,11 +1399,9 @@ export class CanvasEditor {
|
|||||||
paintShape(drag.node, "rectangle", box);
|
paintShape(drag.node, "rectangle", box);
|
||||||
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
|
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
|
||||||
const items = [...drag.base];
|
const items = [...drag.base];
|
||||||
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
const selected = new Set(items);
|
||||||
const item = this.nodes.get(node);
|
for (const item of drag.page.hitIndex.queryBox(box)) {
|
||||||
if (!item.parent && !items.includes(item) && item.x >= box.x && item.x+item.width <= box.x+box.width && item.y >= box.y && item.y+item.height <= box.y+box.height) {
|
if (!selected.has(item)) items.push(item);
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.setSelection(items);
|
this.setSelection(items);
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-49
@@ -38,8 +38,8 @@ import (
|
|||||||
"github.com/xiaoqidun/ofdgo"
|
"github.com/xiaoqidun/ofdgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sessionTextCacheLimit 限制会话保留的页面文字索引数量
|
// sessionTextCacheBytes 限制会话文字索引的内存预算
|
||||||
const sessionTextCacheLimit = 32
|
const sessionTextCacheBytes = 32 << 20
|
||||||
|
|
||||||
// OpenOptions 打开OFD文档选项
|
// OpenOptions 打开OFD文档选项
|
||||||
type OpenOptions struct {
|
type OpenOptions struct {
|
||||||
@@ -63,8 +63,7 @@ type Session struct {
|
|||||||
doc *ofdgo.Document
|
doc *ofdgo.Document
|
||||||
pageCache map[int]*ofdgo.PageContent
|
pageCache map[int]*ofdgo.PageContent
|
||||||
boxCache []pageBoxInfo
|
boxCache []pageBoxInfo
|
||||||
textCache map[int]*ofdgo.PageText
|
textCache *ofdgo.PageTextCache
|
||||||
textOrder []int
|
|
||||||
svgFonts map[string][]byte
|
svgFonts map[string][]byte
|
||||||
fontScan *ofdgo.FontInfoScanner
|
fontScan *ofdgo.FontInfoScanner
|
||||||
fontInfos []FontInfo
|
fontInfos []FontInfo
|
||||||
@@ -355,10 +354,11 @@ type apiResult struct {
|
|||||||
ReasonCode ofdgo.EditReason `json:"reasonCode,omitempty"`
|
ReasonCode ofdgo.EditReason `json:"reasonCode,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
|
// editorClipboard 保存选区标识及同文档快照或跨文档副本
|
||||||
type editorClipboard struct {
|
type editorClipboard struct {
|
||||||
token string
|
token string
|
||||||
selection *ofdgo.ObjectSelection
|
selection *ofdgo.ObjectSelection
|
||||||
|
packet *ofdgo.Editor
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorInfo 编辑文档信息与操作状态
|
// editorInfo 编辑文档信息与操作状态
|
||||||
@@ -451,23 +451,27 @@ type pendingPDF struct {
|
|||||||
font platformValue
|
font platformValue
|
||||||
operation *outputContext
|
operation *outputContext
|
||||||
report func(string, int, int) error
|
report func(string, int, int) error
|
||||||
|
stage string
|
||||||
|
completed int
|
||||||
|
total int
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
pdf *pendingPDF
|
||||||
pdfWarnings []byte
|
pdfWarnings []byte
|
||||||
saveSnapshot *ofdgo.Editor
|
saveSnapshot *ofdgo.Editor
|
||||||
printDocument *ofdgo.PrintDocument
|
printDocument *ofdgo.PrintDocument
|
||||||
pendingImport *ofdgo.Reader
|
pendingImport *ofdgo.Reader
|
||||||
copiedObjects *editorClipboard
|
copiedObjects *editorClipboard
|
||||||
copiedStyle *ofdgo.GraphicObject
|
portableObjects *editorClipboard
|
||||||
ctx context.Context
|
copiedStyle *ofdgo.GraphicObject
|
||||||
resolveInput func(context.Context, any) (io.ReaderAt, int64, error)
|
ctx context.Context
|
||||||
conversionPool *ofdgo.ConversionPool
|
resolveInput func(context.Context, any) (io.ReaderAt, int64, error)
|
||||||
|
conversionPool *ofdgo.ConversionPool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open 打开浏览器内存中的OFD文档
|
// Open 打开浏览器内存中的OFD文档
|
||||||
@@ -596,8 +600,7 @@ func (s *Session) SetFonts(fonts []FontFile) error {
|
|||||||
s.Renderer.SetFontFS()
|
s.Renderer.SetFontFS()
|
||||||
s.fontFS = nil
|
s.fontFS = nil
|
||||||
}
|
}
|
||||||
clear(s.textCache)
|
s.textCache.Clear()
|
||||||
s.textOrder = nil
|
|
||||||
clear(s.svgFonts)
|
clear(s.svgFonts)
|
||||||
s.resetFontInfo()
|
s.resetFontInfo()
|
||||||
return nil
|
return nil
|
||||||
@@ -623,24 +626,13 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
|
|||||||
// 入参: index 页面索引
|
// 入参: index 页面索引
|
||||||
// 返回: *ofdgo.PageText 文字索引,未缓存时为nil
|
// 返回: *ofdgo.PageText 文字索引,未缓存时为nil
|
||||||
func (s *Session) cachedPageText(index int) *ofdgo.PageText {
|
func (s *Session) cachedPageText(index int) *ofdgo.PageText {
|
||||||
text := s.textCache[index]
|
return s.textCache.Get(index)
|
||||||
if text != nil {
|
|
||||||
s.cachePageText(index, text)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cachePageText 保留最近访问的文字索引,避免全文搜索持续占用内存
|
// cachePageText 按内存预算保留页面文字索引,复用阅读与搜索结果
|
||||||
// 入参: index 页面索引, text 文字索引
|
// 入参: index 页面索引, text 文字索引
|
||||||
func (s *Session) cachePageText(index int, text *ofdgo.PageText) {
|
func (s *Session) cachePageText(index int, text *ofdgo.PageText) {
|
||||||
if position := slices.Index(s.textOrder, index); position >= 0 {
|
s.textCache.Put(index, text)
|
||||||
s.textOrder = slices.Delete(s.textOrder, position, position+1)
|
|
||||||
} else if len(s.textOrder) == sessionTextCacheLimit {
|
|
||||||
delete(s.textCache, s.textOrder[0])
|
|
||||||
s.textOrder = slices.Delete(s.textOrder, 0, 1)
|
|
||||||
}
|
|
||||||
s.textOrder = append(s.textOrder, index)
|
|
||||||
s.textCache[index] = text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageTextString 获取页面原文,不为全文复制保留额外的页面和文字索引
|
// PageTextString 获取页面原文,不为全文复制保留额外的页面和文字索引
|
||||||
@@ -879,8 +871,7 @@ func (s *Session) SetRenderBackend(name string) error {
|
|||||||
}
|
}
|
||||||
previous, next := s.Renderer.Backends().Info(), backends.Info()
|
previous, next := s.Renderer.Backends().Info(), backends.Info()
|
||||||
if previous.Compiler != next.Compiler || previous.Fonts != next.Fonts || previous.Geometry != next.Geometry {
|
if previous.Compiler != next.Compiler || previous.Fonts != next.Fonts || previous.Geometry != next.Geometry {
|
||||||
clear(s.textCache)
|
s.textCache.Clear()
|
||||||
s.textOrder = nil
|
|
||||||
}
|
}
|
||||||
ofdgo.WithRenderBackends(backends)(s.Renderer)
|
ofdgo.WithRenderBackends(backends)(s.Renderer)
|
||||||
return nil
|
return nil
|
||||||
@@ -984,7 +975,7 @@ func newSession(reader *ofdgo.Reader, opts OpenOptions) (*Session, error) {
|
|||||||
doc: doc,
|
doc: doc,
|
||||||
pageCache: make(map[int]*ofdgo.PageContent),
|
pageCache: make(map[int]*ofdgo.PageContent),
|
||||||
boxCache: make([]pageBoxInfo, len(doc.Pages.Page)),
|
boxCache: make([]pageBoxInfo, len(doc.Pages.Page)),
|
||||||
textCache: make(map[int]*ofdgo.PageText),
|
textCache: ofdgo.NewPageTextCache(sessionTextCacheBytes),
|
||||||
svgFonts: make(map[string][]byte),
|
svgFonts: make(map[string][]byte),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -1494,7 +1485,7 @@ func (e *businessEngine) openPDF(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
password = slices.Clone(password)
|
password = slices.Clone(password)
|
||||||
}
|
}
|
||||||
pending.operation, pending.report = e.newEditorOperationContext(pending.progress)
|
e.bindPDFOperation(pending)
|
||||||
options := ofdgo.PDFImportOptions{Resources: e.resources, PasswordUTF8: true, Password: password,
|
options := ofdgo.PDFImportOptions{Resources: e.resources, PasswordUTF8: true, Password: password,
|
||||||
OnProgress: func(stage string, completed, total int) error {
|
OnProgress: func(stage string, completed, total int) error {
|
||||||
return pending.report(stage, completed, total)
|
return pending.report(stage, completed, total)
|
||||||
@@ -1624,13 +1615,27 @@ func (e *businessEngine) checkDocument(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch name {
|
switch name {
|
||||||
case "ofdgoCheckDocument", "ofdgoPDFContext", "ofdgoOpenPDF", "ofdgoOpen", "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks", "ofdgoDiscardPrint", "ofdgoDiscardSave", "ofdgoDetectFormat", "ofdgoConvertFile", "ofdgoPackFiles", "ofdgoLoadImport", "ofdgoCreateDocument", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF":
|
case "ofdgoDiscardObjects", "ofdgoExportObjects", "ofdgoCheckDocument", "ofdgoPDFContext", "ofdgoOpenPDF", "ofdgoOpen", "ofdgoLoadPDF", "ofdgoConfigure", "ofdgoDocumentInfo", "ofdgoSVGFontData", "ofdgoRenderBackends", "ofdgoOutputFormats", "ofdgoExportFormats", "ofdgoParsePageRange", "ofdgoMatchFontFiles", "ofdgoOutlineLinks", "ofdgoDocumentNavigation", "ofdgoInvoices", "ofdgoRenderPage", "ofdgoSearchPage", "ofdgoPageTextString", "ofdgoOpenLinks", "ofdgoDiscardPrint", "ofdgoDiscardSave", "ofdgoDetectFormat", "ofdgoConvertFile", "ofdgoPackFiles", "ofdgoLoadImport", "ofdgoCreateDocument", "ofdgoLayoutPrint", "ofdgoPrintSide", "ofdgoPrintPDF":
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return ofdgo.ErrIncompleteDocument
|
return ofdgo.ErrIncompleteDocument
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bindPDFOperation 更新取消上下文并保留转换阶段,时间片检查不重置进度
|
||||||
|
// 入参: pending 当前转换任务
|
||||||
|
func (e *businessEngine) bindPDFOperation(pending *pendingPDF) {
|
||||||
|
operation, report := e.newEditorOperationContext(pending.progress)
|
||||||
|
pending.operation = operation
|
||||||
|
pending.report = func(stage string, completed, total int) error {
|
||||||
|
pending.stage, pending.completed, pending.total = stage, completed, total
|
||||||
|
return report(stage, completed, total)
|
||||||
|
}
|
||||||
|
operation.checkpoint = func() error {
|
||||||
|
return report(pending.stage, pending.completed, pending.total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// preparePDF 只为页面读取和后台加载绑定转换上下文,不补全其他命令
|
// preparePDF 只为页面读取和后台加载绑定转换上下文,不补全其他命令
|
||||||
// 入参: name 命令名称, args 命令参数
|
// 入参: name 命令名称, args 命令参数
|
||||||
// 返回: error 转换或取消错误
|
// 返回: error 转换或取消错误
|
||||||
@@ -1643,7 +1648,7 @@ func (e *businessEngine) preparePDF(name string, args []platformValue) error {
|
|||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
e.pdf.operation, e.pdf.report = e.newEditorOperationContext(e.pdf.progress)
|
e.bindPDFOperation(e.pdf)
|
||||||
e.pdf.operation.checked = time.Now()
|
e.pdf.operation.checked = time.Now()
|
||||||
e.pdf.operation.interval = 255
|
e.pdf.operation.interval = 255
|
||||||
e.currentSession.Renderer.OutputContext = e.pdf.operation
|
e.currentSession.Renderer.OutputContext = e.pdf.operation
|
||||||
@@ -1922,8 +1927,7 @@ func (e *businessEngine) configureDocument(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
if e.currentSession.Renderer.RenderAnnotations != args[1].Bool() {
|
if e.currentSession.Renderer.RenderAnnotations != args[1].Bool() {
|
||||||
e.currentSession.Renderer.RenderAnnotations = args[1].Bool()
|
e.currentSession.Renderer.RenderAnnotations = args[1].Bool()
|
||||||
clear(e.currentSession.textCache)
|
e.currentSession.textCache.Clear()
|
||||||
e.currentSession.textOrder = nil
|
|
||||||
}
|
}
|
||||||
return e.currentSession.Summary(), nil
|
return e.currentSession.Summary(), nil
|
||||||
}
|
}
|
||||||
@@ -5435,7 +5439,7 @@ func (e *businessEngine) copyObjects(args []platformValue) (any, error) {
|
|||||||
|
|
||||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||||
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
// 入参: args 页面索引、对象路径数组和剪贴板标识
|
||||||
// 返回: any 可跨文档导入的选区包, error 错误信息
|
// 返回: any 捕获结果, error 错误信息
|
||||||
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
||||||
if e.currentEditor == nil {
|
if e.currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
@@ -5449,14 +5453,46 @@ func (e *businessEngine) captureObjects(args []platformValue) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var output bytes.Buffer
|
packet, err := e.currentEditor.PrepareTargets(e.operationContext(), args[0].Int(), paths, true)
|
||||||
if _, err := e.currentEditor.WriteTargetsTo(context.Background(), &output, args[0].Int(), paths, true); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
e.copiedObjects = clipboard
|
e.copiedObjects = clipboard
|
||||||
|
e.portableObjects = &editorClipboard{token: clipboard.token, packet: packet}
|
||||||
|
return successResult(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportObjects 按需压缩已准备的独立选区,切换文档或版本不影响结果
|
||||||
|
// 入参: args 剪贴板标识
|
||||||
|
// 返回: any 可跨文档导入的选区包, error 标识或写入错误
|
||||||
|
func (e *businessEngine) exportObjects(args []platformValue) (any, error) {
|
||||||
|
clipboard := e.portableObjects
|
||||||
|
if clipboard == nil || clipboard.token != args[0].String() {
|
||||||
|
return nil, fmt.Errorf("object clipboard is no longer available")
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
if _, err := clipboard.packet.WriteToWithOptions(e.operationContext(), &output, ofdgo.WriteOptions{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.portableObjects = nil
|
||||||
|
clipboard.packet = nil
|
||||||
return successResult(map[string]any{"bytes": bytesToJS(output.Bytes())}), nil
|
return successResult(map[string]any{"bytes": bytesToJS(output.Bytes())}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discardObjects 释放被新剪贴板替代的选区,不影响后续捕获
|
||||||
|
// 入参: args 剪贴板标识
|
||||||
|
// 返回: any 清理结果, error 始终为nil
|
||||||
|
func (e *businessEngine) discardObjects(args []platformValue) (any, error) {
|
||||||
|
token := args[0].String()
|
||||||
|
if e.portableObjects != nil && e.portableObjects.token == token {
|
||||||
|
e.portableObjects = nil
|
||||||
|
}
|
||||||
|
if e.copiedObjects != nil && e.copiedObjects.token == token {
|
||||||
|
e.copiedObjects = nil
|
||||||
|
}
|
||||||
|
return successResult(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||||
// 入参: args 目标页、剪贴板标识和横纵位移
|
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
@@ -6847,7 +6883,7 @@ func (e *businessEngine) syncEditorPageIDs(index int, content *ofdgo.PageContent
|
|||||||
before.PathObject.ID = object.PathObject.ID
|
before.PathObject.ID = object.PathObject.ID
|
||||||
before.ImageObject.ID = object.ImageObject.ID
|
before.ImageObject.ID = object.ImageObject.ID
|
||||||
before.CompositeGraphicUnit.ID = object.CompositeGraphicUnit.ID
|
before.CompositeGraphicUnit.ID = object.CompositeGraphicUnit.ID
|
||||||
delete(e.currentSession.textCache, index)
|
e.currentSession.textCache.Delete(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6890,11 +6926,12 @@ func (e *businessEngine) previewEditor(editor *ofdgo.Editor, annotations bool) (
|
|||||||
session.pageCache[index] = page
|
session.pageCache[index] = page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, index := range e.currentSession.textOrder {
|
e.currentSession.textCache.Range(func(index int, text *ofdgo.PageText) bool {
|
||||||
if !slices.Contains(changed, index) {
|
if !slices.Contains(changed, index) {
|
||||||
session.cachePageText(index, e.currentSession.textCache[index])
|
session.cachePageText(index, text)
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
for index, box := range e.currentSession.boxCache {
|
for index, box := range e.currentSession.boxCache {
|
||||||
if !slices.Contains(changed, index) {
|
if !slices.Contains(changed, index) {
|
||||||
session.boxCache[index] = box
|
session.boxCache[index] = box
|
||||||
@@ -7978,6 +8015,8 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an
|
|||||||
"ofdgoEraseObjectsPath": e.eraseObjectsPath,
|
"ofdgoEraseObjectsPath": e.eraseObjectsPath,
|
||||||
"ofdgoCopyObjects": e.copyObjects,
|
"ofdgoCopyObjects": e.copyObjects,
|
||||||
"ofdgoCaptureObjects": e.captureObjects,
|
"ofdgoCaptureObjects": e.captureObjects,
|
||||||
|
"ofdgoExportObjects": e.exportObjects,
|
||||||
|
"ofdgoDiscardObjects": e.discardObjects,
|
||||||
"ofdgoPasteObjects": e.pasteObjects,
|
"ofdgoPasteObjects": e.pasteObjects,
|
||||||
"ofdgoImportObjects": e.importObjects,
|
"ofdgoImportObjects": e.importObjects,
|
||||||
"ofdgoOrderObjects": e.orderObjects,
|
"ofdgoOrderObjects": e.orderObjects,
|
||||||
|
|||||||
@@ -227,8 +227,7 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
|
|||||||
if result != nil && result["ok"] == true && ctx.Err() == nil {
|
if result != nil && result["ok"] == true && ctx.Err() == nil {
|
||||||
e.currentSession.svgFonts = maps.Clone(worker.svgFonts)
|
e.currentSession.svgFonts = maps.Clone(worker.svgFonts)
|
||||||
clear(worker.pageCache)
|
clear(worker.pageCache)
|
||||||
clear(worker.textCache)
|
worker.textCache.Clear()
|
||||||
worker.textOrder = nil
|
|
||||||
clear(worker.svgFonts)
|
clear(worker.svgFonts)
|
||||||
e.renderIdle = append(e.renderIdle, worker)
|
e.renderIdle = append(e.renderIdle, worker)
|
||||||
}
|
}
|
||||||
@@ -380,7 +379,7 @@ func (e *NativeEngine) Close() error {
|
|||||||
if e.pendingImport != nil {
|
if e.pendingImport != nil {
|
||||||
err = errors.Join(err, e.pendingImport.Close())
|
err = errors.Join(err, e.pendingImport.Close())
|
||||||
}
|
}
|
||||||
e.currentSession, e.currentEditor, e.pendingImport, e.copiedObjects, e.copiedStyle = nil, nil, nil, nil, nil
|
e.currentSession, e.currentEditor, e.pendingImport, e.copiedObjects, e.portableObjects, e.copiedStyle = nil, nil, nil, nil, nil, nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +430,7 @@ func (e *NativeEngine) renderSession(ctx context.Context) (*Session, error) {
|
|||||||
}
|
}
|
||||||
return &Session{Reader: renderer.Reader, Renderer: renderer, doc: source.doc, fontFS: source.fontFS,
|
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...),
|
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
|
textCache: ofdgo.NewPageTextCache(sessionTextCacheBytes), svgFonts: make(map[string][]byte), editing: source.editing}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareRender 同步当前编辑页面的图元标识,避免并行访问编辑器缓存
|
// prepareRender 同步当前编辑页面的图元标识,避免并行访问编辑器缓存
|
||||||
|
|||||||
+24
-12
@@ -962,20 +962,20 @@ func (e *Editor) formatTargetsText(page int, paths []ObjectPath, patch TextForma
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTargetsTo 将选区及其关联资源写成独立单页OFD,来源关闭后仍可导入
|
// PrepareTargets 将选区及其关联资源复制为独立单页编辑器,来源关闭后仍可使用
|
||||||
// 不修改来源、历史或组合结构,加密来源须显式允许明文导出
|
// 不修改来源、历史或组合结构,加密来源须显式允许明文导出
|
||||||
// 不包含页面模板和签名,指向其他页面的跳转随单页导出移除
|
// 不包含页面模板和签名,指向其他页面的跳转随单页导出移除
|
||||||
// 入参: ctx 取消上下文, writer 输出流, page 来源页, paths 完整对象路径, allowDecrypted 允许明文导出
|
// 入参: ctx 取消上下文, page 来源页, paths 完整对象路径, allowDecrypted 允许明文导出
|
||||||
// 返回: int64 写入字节数, error 路径、资源或写入错误
|
// 返回: *Editor 独立选区,可直接导出或继续编辑, error 路径或资源错误
|
||||||
func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int, paths []ObjectPath, allowDecrypted bool) (int64, error) {
|
func (e *Editor) PrepareTargets(ctx context.Context, page int, paths []ObjectPath, allowDecrypted bool) (*Editor, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.encryption != nil && !allowDecrypted {
|
if e.encryption != nil && !allowDecrypted {
|
||||||
return 0, ErrEncryptionPolicyRequired
|
return nil, ErrEncryptionPolicyRequired
|
||||||
}
|
}
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
return 0, fmt.Errorf("no objects selected")
|
return nil, fmt.Errorf("no objects selected")
|
||||||
}
|
}
|
||||||
snapshot := e.transactionSnapshot()
|
snapshot := e.transactionSnapshot()
|
||||||
snapshot.history, snapshot.historyIndex, snapshot.historyLimit = nil, 0, 0
|
snapshot.history, snapshot.historyIndex, snapshot.historyLimit = nil, 0, 0
|
||||||
@@ -989,19 +989,19 @@ func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int,
|
|||||||
}
|
}
|
||||||
selection, err := snapshot.CaptureTargets(page, paths)
|
selection, err := snapshot.CaptureTargets(page, paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
content, err := snapshot.page(page)
|
content, err := snapshot.page(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
objects, annotations, err := snapshot.PasteSelection(page, selection, 0, 0)
|
objects, annotations, err := snapshot.PasteSelection(page, selection, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
box, err := creationBox(content.Area.PhysicalBox)
|
box, err := creationBox(content.Area.PhysicalBox)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
packet := NewEditor()
|
packet := NewEditor()
|
||||||
packet.SetRenderBackends(e.backends)
|
packet.SetRenderBackends(e.backends)
|
||||||
@@ -1010,10 +1010,22 @@ func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int,
|
|||||||
packet.SetFontFS(e.fontFS...)
|
packet.SetFontFS(e.fontFS...)
|
||||||
packet.OnWriteProgress = e.OnWriteProgress
|
packet.OnWriteProgress = e.OnWriteProgress
|
||||||
if _, err := packet.AddPage(box.W, box.H); err != nil {
|
if _, err := packet.AddPage(box.W, box.H); err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
packet.pages[0].Area = content.Area
|
packet.pages[0].Area = content.Area
|
||||||
if _, _, err := packet.importSelection(ctx, &snapshot, page, objects, annotations, 0, ObjectImportOptions{AllowDecrypted: true, OnProgress: e.OnWriteProgress}, false); err != nil {
|
if _, _, err := packet.importSelection(ctx, &snapshot, page, objects, annotations, 0, ObjectImportOptions{AllowDecrypted: true, OnProgress: e.OnWriteProgress}, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
packet.history, packet.historyIndex = nil, 0
|
||||||
|
return packet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTargetsTo 将选区及关联资源写成独立单页OFD,不修改来源与历史
|
||||||
|
// 入参: ctx 取消上下文, writer 输出流, page 来源页, paths 完整对象路径, allowDecrypted 允许明文导出
|
||||||
|
// 返回: int64 写入字节数, error 路径、资源或写入错误
|
||||||
|
func (e *Editor) WriteTargetsTo(ctx context.Context, writer io.Writer, page int, paths []ObjectPath, allowDecrypted bool) (int64, error) {
|
||||||
|
packet, err := e.PrepareTargets(ctx, page, paths, allowDecrypted)
|
||||||
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return packet.WriteToWithOptions(ctx, writer, WriteOptions{})
|
return packet.WriteToWithOptions(ctx, writer, WriteOptions{})
|
||||||
|
|||||||
@@ -24,8 +24,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PageTextCache 按内存预算保留最近访问的页面文字索引
|
||||||
|
// 缓存后不得修改PageText,页面或字体变化时须删除对应条目
|
||||||
|
type PageTextCache struct {
|
||||||
|
cache *renderCache[int, *PageText]
|
||||||
|
}
|
||||||
|
|
||||||
// textPageTokens 保留文字及其复合图元和裁剪,跳过独立路径与图片
|
// textPageTokens 保留文字及其复合图元和裁剪,跳过独立路径与图片
|
||||||
type textPageTokens struct {
|
type textPageTokens struct {
|
||||||
*xml.Decoder
|
*xml.Decoder
|
||||||
@@ -76,6 +83,83 @@ type TextMatchPart struct {
|
|||||||
End int `json:"end"`
|
End int `json:"end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPageTextCache 创建文字索引缓存,预算包含字符区域和条目开销
|
||||||
|
// 入参: bytes 内存预算,非正数禁用缓存
|
||||||
|
// 返回: *PageTextCache 独立缓存
|
||||||
|
func NewPageTextCache(bytes int) *PageTextCache {
|
||||||
|
return &PageTextCache{cache: resourceCache[int, *PageText](nil, max(0, bytes))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemorySize 估算文字索引占用,包含切片容量和字符串内容
|
||||||
|
// 返回: int64 估算字节数,共享字符串按各次引用计费
|
||||||
|
func (p *PageText) MemorySize() int64 {
|
||||||
|
size := int64(unsafe.Sizeof(*p)) + int64(cap(p.Runs))*int64(unsafe.Sizeof(TextRun{}))
|
||||||
|
for _, run := range p.Runs {
|
||||||
|
size += int64(len(run.ID)) + int64(len(run.Text))
|
||||||
|
size += int64(cap(run.Boxes))*int64(unsafe.Sizeof(Box{})) + int64(cap(run.Spans))*int64(unsafe.Sizeof(TextSpan{}))
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取文字索引并更新最近访问顺序
|
||||||
|
// 入参: index 页面索引
|
||||||
|
// 返回: *PageText 文字索引,未命中时为nil
|
||||||
|
func (c *PageTextCache) Get(index int) *PageText {
|
||||||
|
text, _ := c.cache.get(index)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put 缓存完整文字索引,超出预算的单页不驱逐其他页面
|
||||||
|
// 入参: index 页面索引, text 文字索引,nil删除已有条目
|
||||||
|
func (c *PageTextCache) Put(index int, text *PageText) {
|
||||||
|
if text == nil {
|
||||||
|
c.Delete(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := text.MemorySize() + 128
|
||||||
|
if size > int64(c.cache.limit) {
|
||||||
|
c.Delete(index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.cache.put(index, text, int(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除页面索引并释放预算
|
||||||
|
// 入参: index 页面索引
|
||||||
|
func (c *PageTextCache) Delete(index int) {
|
||||||
|
c.cache.mu.Lock()
|
||||||
|
defer c.cache.mu.Unlock()
|
||||||
|
if entry := c.cache.entries[index]; entry != nil {
|
||||||
|
c.cache.remove(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空缓存和计费状态
|
||||||
|
func (c *PageTextCache) Clear() {
|
||||||
|
c.cache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len 返回缓存页面数
|
||||||
|
// 返回: int 已缓存页面数
|
||||||
|
func (c *PageTextCache) Len() int {
|
||||||
|
c.cache.mu.Lock()
|
||||||
|
defer c.cache.mu.Unlock()
|
||||||
|
return len(c.cache.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range 按访问时间从旧到新遍历,遍历期间不得访问当前缓存
|
||||||
|
// 入参: visit 访问函数,返回false停止遍历
|
||||||
|
func (c *PageTextCache) Range(visit func(int, *PageText) bool) {
|
||||||
|
c.cache.mu.Lock()
|
||||||
|
defer c.cache.mu.Unlock()
|
||||||
|
for entry := c.cache.order.Back(); entry != nil; entry = entry.Prev() {
|
||||||
|
value := entry.Value.(renderCacheEntry[int, *PageText])
|
||||||
|
if !visit(value.key, value.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Token 读取文字页面所需的XML节点
|
// Token 读取文字页面所需的XML节点
|
||||||
// 返回: xml.Token XML节点, error 错误信息
|
// 返回: xml.Token XML节点, error 错误信息
|
||||||
func (d *textPageTokens) Token() (xml.Token, error) {
|
func (d *textPageTokens) Token() (xml.Token, error) {
|
||||||
|
|||||||
Reference in new issue
Block a user