From 5dea48d54b2372179a4043056aafec3f5e295fc4 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Thu, 15 Oct 2026 17:11:01 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E8=B5=84=E6=BA=90=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B5=B7=E9=87=8F=E8=B5=84=E6=BA=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/index.html | 2 +- assets/webui/ofdgo.css | 5 +++ assets/webui/ofdgo.js | 92 +++++++++++++++++++++++++++----------- assets/webui/ofdgo_wasm.js | 10 +++-- internal/webui/wasm.go | 25 ++++++++--- ofdgo_editor_font_scan.go | 14 +++++- ofdgo_resource.go | 61 +++++++++++++++++++++++-- 7 files changed, 169 insertions(+), 40 deletions(-) diff --git a/assets/webui/index.html b/assets/webui/index.html index c744af1..3fc66e0 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -370,7 +370,7 @@ -
+
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 99b9901..dbcbe74 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -1670,6 +1670,11 @@ body[data-hide-meta] .meta-panel { min-height: 80px; } +#resourceReferenceGroup { + flex: 0 0 110px; + width: 110px; +} + .resource-option { width: 100%; border: 0; diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 6401a94..e583e6d 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -23,8 +23,8 @@ const batch = { items: [], formats: [], running: false, canceled: false, worker: const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)])); const fontBinding = { font: null, choices: [], sequence: 0 }; const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)])); -const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Actions", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "BindingActions", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)])); -const resourceManager = { items: [], selected: null, targets: [], pick: null, busy: false, sequence: 0, url: null, fileAction: null }; +const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "ReferenceGroup", "Actions", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "BindingActions", "Page", "Object", "Pick", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)])); +const resourceManager = { items: [], selected: null, references: [], referenceOffset: 0, targets: [], pick: null, busy: false, sequence: 0, url: null, fileAction: null }; const settingsElements = Object.fromEntries(["Button", "Panel", "Form", "Title", "Kind", "Context", "Content", "Actions", "Status"].map(name => [name, document.querySelector(`#settings${name}`)])); const settings = { sequence: 0, busy: false, page: 0, scope: "document", object: "", path: [], selected: -1, data: null, actions: [], drafts: new Map(), view: "", baseline: {}, suspended: false }; @@ -758,10 +758,14 @@ editorClick(el.documentManageButton, openDocumentManagement); resourceElements.ManageButton.addEventListener("click", openResourceManagement); resourceElements.Close.addEventListener("click", () => resourceElements.Panel.close()); resourceElements.Form.addEventListener("submit", event => event.preventDefault()); -resourceElements.Panel.addEventListener("cancel", event => { if (resourceManager.busy) event.preventDefault(); }); -resourceElements.Panel.addEventListener("close", () => { resourceManager.sequence++; releaseResourcePreview(); }); +resourceElements.Panel.addEventListener("close", () => { + resourceManager.sequence++; + if (resourceManager.requestID) wasmWorker?.postMessage({type: "cancel", id: resourceManager.requestID}); + releaseResourcePreview(); +}); resourceElements.Kind.addEventListener("change", () => resourceTask(renderResourceList)); resourceElements.Reference.addEventListener("change", () => resourceTask(syncResourceReference)); +resourceElements.ReferenceGroup.addEventListener("change", () => resourceTask(() => loadManagedReferences(Number(resourceElements.ReferenceGroup.value) * 100))); resourceElements.Page.addEventListener("change", () => resourceTask(() => loadResourceTargets(""))); resourceElements.Object.addEventListener("change", updateResourceControls); resourceElements.Pick.addEventListener("click", () => resourceTask(beginResourcePick)); @@ -6142,7 +6146,7 @@ async function locateSettingsPage(page) { } async function openResourceManagement() { - if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return; + if (resourceManager.busy || !state.doc || !state.ready || document.body.hasAttribute("aria-busy")) return; if (resourceManager.pick) { finishResourcePick(); return; } resourceElements.Panel.showModal(); resourceElements.Page.replaceChildren(...state.doc.pages.map((page, index) => new Option(`第${index + 1}页`, String(index)))); @@ -6153,7 +6157,8 @@ async function openResourceManagement() { async function resourceTask(action) { if (resourceManager.busy) return; resourceManager.busy = true; - resourceElements.Form.inert = true; + resourceElements.List.inert = true; + updateResourceControls(); resourceElements.Status.textContent = "处理中"; resourceElements.Panel.setAttribute("aria-busy", "true"); try { @@ -6163,7 +6168,7 @@ async function resourceTask(action) { resourceElements.Status.textContent = resourceError(err.message); } finally { resourceManager.busy = false; - resourceElements.Form.inert = false; + resourceElements.List.inert = false; resourceElements.Panel.removeAttribute("aria-busy"); updateResourceControls(); } @@ -6180,7 +6185,9 @@ function resourceError(message) { async function loadManagedResources() { const sequence = state.openSeq; - const items = await callWASM("ofdgoResources"); + let items; + try { items = await callWASM("ofdgoResources"); } + finally { resourceManager.requestID = 0; } if (sequence !== state.openSeq || !resourceElements.Panel.open) return; resourceManager.items = items || []; await renderResourceList(); @@ -6191,14 +6198,17 @@ async function renderResourceList() { const selected = items.find(item => item.id === resourceManager.selected?.id && item.kind === resourceManager.selected?.kind) || items[0]; resourceElements.List.replaceChildren(); resourceElements.Empty.hidden = items.length !== 0; + const sequence = resourceManager.sequence; + let fragment = document.createDocumentFragment(); for (const item of items) { const button = document.createElement("button"); button.type = "button"; button.className = "resource-option"; button.setAttribute("role", "option"); button.setAttribute("aria-selected", String(item === selected)); + button.tabIndex = item === selected ? 0 : -1; button.title = item.file || item.name; - for (const value of [item.name, item.missing ? "缺失" : item.file ? formatBytes(item.size) : item.kind === "Font" ? "外部" : "-", item.complete ? String(item.references?.length || 0) : "未知"]) { + for (const value of [item.name, item.missing ? "缺失" : item.file ? formatBytes(item.size) : item.kind === "Font" ? "外部" : "-", item.complete ? String(item.referenceCount || 0) : "未知"]) { const span = document.createElement("span"); span.textContent = value; button.append(span); @@ -6213,8 +6223,15 @@ async function renderResourceList() { if (resourceElements.Panel.open && resourceManager.selected === items[target]) resourceElements.List.children[target]?.focus(); }); }); - resourceElements.List.append(button); + fragment.append(button); + if (fragment.childElementCount === 100) { + resourceElements.List.append(fragment); + await new Promise(resolve => setTimeout(resolve, 0)); + if (sequence !== resourceManager.sequence || !resourceElements.Panel.open) return; + fragment = document.createDocumentFragment(); + } } + resourceElements.List.append(fragment); await selectManagedResource(selected); } @@ -6224,38 +6241,60 @@ async function selectManagedResource(item) { releaseResourcePreview(); resourceManager.selected = item || null; const items = resourceManager.items.filter(entry => entry.kind === resourceElements.Kind.value); - Array.from(resourceElements.List.children).forEach((node, i) => { - node.setAttribute("aria-selected", String(items[i] === item)); - node.tabIndex = items[i] === item ? 0 : -1; - }); - const references = item?.references || []; + const previous = resourceElements.List.querySelector('[aria-selected="true"]'); + if (previous) { previous.setAttribute("aria-selected", "false"); previous.tabIndex = -1; } + const current = resourceElements.List.children[items.indexOf(item)]; + if (current) { current.setAttribute("aria-selected", "true"); current.tabIndex = 0; } + await loadManagedReferences(same ? Number(reference) : 0); +} + +async function loadManagedReferences(index = 0) { + const item = resourceManager.selected, sequence = state.openSeq, panelSequence = resourceManager.sequence; + const count = item?.referenceCount || 0; + index = Math.max(0, Math.min(index, count - 1)); + const offset = Math.floor(index / 100) * 100; + const references = count ? await callWASM("ofdgoResourceReferences", item.kind, item.id, offset, 100) || [] : []; + if (sequence !== state.openSeq || panelSequence !== resourceManager.sequence || !resourceElements.Panel.open || item !== resourceManager.selected) return; + resourceManager.references = references; + resourceManager.referenceOffset = offset; const roles = {Font: "字体", ResourceID: "资源", Substitution: "替换", ImageMask: "蒙版", Thumbnail: "缩略", AttachID: "附件"}; - resourceElements.Reference.replaceChildren(...references.map((ref, index) => new Option(`${ref.page >= 0 ? `第${ref.page + 1}页` : ref.file}${ref.object ? ` · ${ref.object}` : ""} · ${roles[ref.role] || ref.role}`, String(index)))); + resourceElements.Reference.replaceChildren(...references.map((ref, index) => new Option(`${ref.page >= 0 ? `第${ref.page + 1}页` : ref.file}${ref.object ? ` · ${ref.object}` : ""} · ${roles[ref.role] || ref.role}`, String(offset + index)))); if (!references.length) resourceElements.Reference.add(new Option(!item || item.complete ? "暂无引用" : "引用未知", "")); - else resourceElements.Reference.value = references[Number(reference)] ? String(Number(reference)) : "0"; + else resourceElements.Reference.value = String(index); + resourceElements.Reference.title = count ? `${offset + 1}-${offset + references.length} / ${count}` : ""; + const groups = Math.ceil(count / 100); + if (resourceElements.ReferenceGroup.options.length !== groups) { + const fragment = document.createDocumentFragment(); + for (let group = 0; group < groups; group++) fragment.append(new Option(`${group + 1} / ${groups}`, String(group))); + resourceElements.ReferenceGroup.replaceChildren(fragment); + } + resourceElements.ReferenceGroup.value = String(offset / 100); updateResourceControls(); - if (same) await loadResourceTargets(); - else await syncResourceReference(); + await syncResourceReference(); } function updateResourceControls() { const item = resourceManager.selected, editing = state.editing; const disabled = resourceManager.busy || !state.doc || document.body.hasAttribute("aria-busy"); + resourceElements.Kind.disabled = resourceElements.Page.disabled = resourceElements.Object.disabled = disabled; + const referenceCount = item?.referenceCount || 0; + resourceElements.ReferenceGroup.hidden = referenceCount <= 100; + resourceElements.ReferenceGroup.disabled = disabled || referenceCount <= 100; const actionable = item && ["Audio", "Video", "Attachment"].includes(item.kind); resourceElements.Add.hidden = resourceElements.Replace.hidden = resourceElements.Delete.hidden = !editing; resourceElements.Rename.hidden = !editing || item?.kind !== "Attachment"; resourceElements.Rename.disabled = disabled || item?.kind !== "Attachment"; resourceElements.Add.disabled = disabled; - resourceElements.Reference.disabled = disabled || !item?.references?.length; + resourceElements.Reference.disabled = disabled || !referenceCount; const style = item && ["ColorSpace", "DrawParam"].includes(item.kind); resourceElements.Preview.disabled = disabled || (!style && (!item?.file || item.missing || item.kind === "Attachment")); resourceElements.Preview.textContent = style ? "查看" : "预览"; resourceElements.Export.disabled = disabled || !item?.file || item.missing; resourceElements.Replace.disabled = disabled || !item || item.kind !== "Attachment" && !item.complete; resourceElements.Replace.textContent = style ? "修改" : item?.kind === "Font" ? item.file ? "外置" : "嵌入" : "替换"; - resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(item.references?.length)); - resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : item?.references?.length ? "资源仍被引用" : "删除资源"; - resourceElements.Locate.disabled = disabled || !item?.references?.length; + resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(referenceCount)); + resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : referenceCount ? "资源仍被引用" : "删除资源"; + resourceElements.Locate.disabled = disabled || !referenceCount; resourceElements.Binding.hidden = !editing || !actionable; const actions = resourceElements.Binding.hidden ? resourceElements.Actions : resourceElements.BindingActions; if (resourceElements.Close.parentElement !== actions) actions.append(resourceElements.Close); @@ -6266,7 +6305,7 @@ function updateResourceControls() { } async function syncResourceReference() { - const reference = resourceManager.selected?.references?.[Number(resourceElements.Reference.value)]; + const reference = resourceManager.references[Number(resourceElements.Reference.value) - resourceManager.referenceOffset]; if (reference?.page >= 0) resourceElements.Page.value = String(reference.page); await loadResourceTargets(reference?.page >= 0 ? reference.object : ""); updateResourceControls(); @@ -6275,7 +6314,7 @@ async function syncResourceReference() { async function loadResourceTargets(preferred) { const resource = resourceManager.selected; if (preferred === undefined) { - const reference = resource?.references?.[Number(resourceElements.Reference.value)]; + const reference = resourceManager.references[Number(resourceElements.Reference.value) - resourceManager.referenceOffset]; preferred = resourceElements.Object.value || (reference?.page === Number(resourceElements.Page.value) ? reference.object : ""); } resourceManager.targets = []; @@ -6294,7 +6333,7 @@ async function loadResourceTargets(preferred) { async function locateManagedResource() { const item = resourceManager.selected, sequence = state.openSeq; - if (!item?.references?.length) return; + if (!item?.referenceCount) return; const locations = await callWASM("ofdgoResourceLocations", item.kind, item.id, Number(resourceElements.Reference.value)); if (sequence !== state.openSeq || !resourceElements.Panel.open || item !== resourceManager.selected) return; if (!locations?.length) { resourceElements.Status.textContent = "暂无可见位置"; return; } @@ -10976,6 +11015,7 @@ async function callWASM(name, ...args) { } return new Promise((resolve, reject) => { const id = ++wasmRequestID; + if (name === "ofdgoResources" && resourceElements.Panel.open) resourceManager.requestID = id; wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq }); if (name === "ofdgoExportMedia") wasmRequests.get(id).mediaID = args[0]; if (name === "ofdgoConvertPDF") { diff --git a/assets/webui/ofdgo_wasm.js b/assets/webui/ofdgo_wasm.js index f79740e..0ad7db4 100644 --- a/assets/webui/ofdgo_wasm.js +++ b/assets/webui/ofdgo_wasm.js @@ -29,6 +29,7 @@ self.onmessage = ({ data }) => { operations.get(data.id)?.abort(); return; } + if (data.name === "ofdgoResources") operations.set(data.id, new AbortController()); if (data.name === "ofdgoImportImages" || data.name === "ofdgoExportImages" || data.name === "ofdgoConvertFile" || data.name === "ofdgoPackFiles" || data.name === "ofdgoConvertPDF" || data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoExportResource" || data.name === "ofdgoExportMedia" || data.name === "ofdgoExportVersion" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") { operations.set(data.id, new AbortController()); } @@ -168,6 +169,7 @@ async function handleMessage({ id, name, args }) { const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages"; const converting = name === "ofdgoConvertPDF"; const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveSigned"; + const resources = name === "ofdgoResources"; const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null; const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null; const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null; @@ -210,7 +212,9 @@ async function handleMessage({ id, name, args }) { }; channel.port2.postMessage(null); }; - if (importing || converting) { + if (resources) { + args.push(done => finish(done)); + } else if (importing || converting) { args.push((phase, completed, total, done) => { self.postMessage({ id, type: importing ? "import" : "conversion", phase, completed, total }); finish(done, null, importing && phase === "commit"); @@ -259,7 +263,7 @@ async function handleMessage({ id, name, args }) { const payload = await globalThis[name](...args); signal?.throwIfAborted(); const result = typeof payload === "string" ? JSON.parse(payload) : payload; - if (signal && !importing && !converting && result.ok) { + if (signal && !importing && !converting && !resources && result.ok) { operations.delete(id); result.data.size = size; if (name !== "ofdgoExportMedia") self.postMessage({ id, type: "export", stage: "save" }); @@ -278,7 +282,7 @@ async function handleMessage({ id, name, args }) { await output?.abort().catch(() => {}); const result = { id, ok: false, error: err.message }; if (signal?.aborted) { - result.error = importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消"; + result.error = resources ? "查询已取消" : importing ? "导入已取消" : converting ? "转换已取消" : "导出已取消"; result.canceled = true; } self.postMessage(result); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 1635d35..8272c46 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -234,7 +234,8 @@ func RunWASM() { registerAsyncCallback("ofdgoExportDocument", exportDocument) registerAsyncCallback("ofdgoExportAttachment", exportAttachment) registerAsyncCallback("ofdgoExportMedia", exportMedia) - registerCallback("ofdgoResources", documentResources) + registerAsyncCallback("ofdgoResources", documentResources) + registerCallback("ofdgoResourceReferences", documentResourceReferences) registerCallback("ofdgoResourceTargets", resourceTargets) registerCallback("ofdgoResourceLocations", resourceLocations) registerCallback("ofdgoFontPreview", fontPreview) @@ -816,14 +817,28 @@ func exportMedia(args []js.Value) (any, error) { return successResult(map[string]any{"format": media.Format, "kind": media.Type}), nil } -// documentResources 读取当前文档和版本的资源及引用,不执行媒体动作 -// 入参: args 无参数 -// 返回: any 资源列表, error 读取错误 +// documentResources 读取当前文档和版本的资源摘要及引用总数,不执行媒体动作 +// 入参: args 可选的取消检查回调 +// 返回: any 资源摘要, error 读取错误 func documentResources(args []js.Value) (any, error) { if currentSession == nil { return nil, fmt.Errorf("ofd document is not opened") } - return currentSession.Reader.Resources(context.Background()) + ctx := context.Background() + if len(args) == 1 { + ctx = newOutputContext(args[0]) + } + return currentSession.Reader.ResourceSummaries(ctx) +} + +// documentResourceReferences 分页读取选中资源的引用位置 +// 入参: args 资源类型、标识、零基起始位置和最大数量 +// 返回: any 引用列表, error 参数或读取错误 +func documentResourceReferences(args []js.Value) (any, error) { + if currentSession == nil || len(args) != 4 { + return nil, fmt.Errorf("invalid resource reference request") + } + return currentSession.Reader.ResourceReferences(context.Background(), args[0].String(), args[1].String(), args[2].Int(), args[3].Int()) } // resourceTargets 列出当前页可绑定指定资源的图元及已有绑定状态 diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index 487638f..b5a94ac 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -67,6 +67,13 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon // 入参: ctx 取消上下文, reader 包快照, index 文档索引, inspect 可选的文件节点访问器 // 返回: *editorFontDocument 字体引用, error 读取、结构或访问错误 func scanEditorFontFiles(ctx context.Context, reader *Reader, index int, inspect func(*editorFontXML, *editorXML) error) (*editorFontDocument, error) { + return scanEditorFontFilesMode(ctx, reader, index, inspect, true) +} + +// scanEditorFontFilesMode 按需保留文字节点及原文,只读访问在扫描后释放非资源文件原文 +// 入参: ctx 取消上下文, reader 包快照, index 文档索引, inspect 可选的文件节点访问器, retain 是否保留编辑数据 +// 返回: *editorFontDocument 扫描结果, error 读取、结构或访问错误 +func scanEditorFontFilesMode(ctx context.Context, reader *Reader, index int, inspect func(*editorFontXML, *editorXML) error, retain bool) (*editorFontDocument, error) { scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration), texts: make(map[string][]editorFontText)} entry, err := reader.documentEntryContext(ctx, index) if err != nil { @@ -146,7 +153,9 @@ func scanEditorFontFiles(ctx context.Context, reader *Reader, index int, inspect } if node.matchesOFD("TextObject") || node.matchesOFD("Text") && node.parent != nil && node.parent.matchesOFD("Area") { id := editorResourceID(node.attr("Font")) - scan.texts[id] = append(scan.texts[id], editorFontText{file: file, node: node}) + if retain { + scan.texts[id] = append(scan.texts[id], editorFontText{file: file, node: node}) + } if id == "" { scan.unsafe = true } @@ -184,6 +193,9 @@ func scanEditorFontFiles(ctx context.Context, reader *Reader, index int, inspect return nil, err } } + if !retain && !root.matchesOFD("Res") { + file.data = nil + } } if err := ctx.Err(); err != nil { return nil, err diff --git a/ofdgo_resource.go b/ofdgo_resource.go index c3d08ee..837ad45 100644 --- a/ofdgo_resource.go +++ b/ofdgo_resource.go @@ -41,6 +41,12 @@ type ResourceInfo struct { References []ResourceReference `json:"references"` } +// ResourceSummary 保存资源摘要及引用总数,内嵌ResourceInfo的References为空 +type ResourceSummary struct { + ResourceInfo + ReferenceCount int `json:"referenceCount"` +} + // ResourceReference 记录资源引用所在的XML及字段,Page为零基页码,非页面或多实例文件为-1 // Object为最近的图元标识,模板、注解、图案和复合资源通过File区分,不推断其页面实例数 type ResourceReference struct { @@ -80,6 +86,46 @@ func (r *Reader) Resources(ctx context.Context) ([]ResourceInfo, error) { return result, nil } +// ResourceSummaries 列出资源摘要及引用总数,不复制引用明细 +// 入参: ctx 取消上下文 +// 返回: []ResourceSummary 资源摘要, error 查询错误 +func (r *Reader) ResourceSummaries(ctx context.Context) ([]ResourceSummary, error) { + items, err := r.resourceList(ctx) + if err != nil { + return nil, err + } + result := make([]ResourceSummary, len(items)) + for i, item := range items { + if err := ctx.Err(); err != nil { + return nil, err + } + result[i] = ResourceSummary{ResourceInfo: item, ReferenceCount: len(item.References)} + result[i].References = nil + } + return result, ctx.Err() +} + +// ResourceReferences 分页读取当前文档及版本的资源引用,顺序与Resources一致 +// 入参: ctx 取消上下文, kind 类型, id 标识, offset 起始位置, limit 最大数量 +// 返回: []ResourceReference 独立引用副本, error 查询或分页参数错误 +func (r *Reader) ResourceReferences(ctx context.Context, kind, id string, offset, limit int) ([]ResourceReference, error) { + if offset < 0 || limit < 1 { + return nil, fmt.Errorf("invalid resource reference range") + } + items, err := r.resourceList(ctx) + if err != nil { + return nil, err + } + item, err := findResource(items, kind, id) + if err != nil { + return nil, err + } + if offset >= len(item.References) { + return []ResourceReference{}, ctx.Err() + } + return slices.Clone(item.References[offset : offset+min(limit, len(item.References)-offset)]), ctx.Err() +} + // OpenResource 打开当前资源的原始文件,ColorSpace导出ICC,无ICC或内联DrawParam不提供文件流 // 外部字体同样无可导出的内嵌文件,定义读取使用ColorSpace或DrawParam // 入参: ctx 取消上下文, kind 资源类型, id 资源标识 @@ -113,7 +159,7 @@ func (r *Reader) resourceList(ctx context.Context) ([]ResourceInfo, error) { if r.resourceInfoCache != nil { return r.resourceInfoCache, nil } - catalog, err := r.resourceCatalog(ctx) + catalog, err := r.resourceCatalogMode(ctx, false) if err != nil { return nil, err } @@ -126,6 +172,13 @@ func (r *Reader) resourceList(ctx context.Context) ([]ResourceInfo, error) { // 入参: ctx 取消上下文 // 返回: *resourceCatalog 资源索引, error 结构或读取错误 func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) { + return r.resourceCatalogMode(ctx, true) +} + +// resourceCatalogMode 只读查询不保留图元节点及页面原文,编辑查询保留原节点 +// 入参: ctx 取消上下文, retain 是否保留编辑节点及原文 +// 返回: *resourceCatalog 资源索引, error 结构或读取错误 +func (r *Reader) resourceCatalogMode(ctx context.Context, retain bool) (*resourceCatalog, error) { catalog := &resourceCatalog{nodes: make(map[string]editorFontText), items: []ResourceInfo{}} pages := make(map[string]int) doc, err := r.DocContext(ctx) @@ -144,7 +197,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) pages[cleanPackagePath(r.ResPath(page.BaseLoc))] = -1 } refs := make(map[string][]ResourceReference) - scan, err := scanEditorFontFiles(ctx, r, r.DocumentIndex(), func(file *editorFontXML, root *editorXML) error { + scan, err := scanEditorFontFilesMode(ctx, r, r.DocumentIndex(), func(file *editorFontXML, root *editorXML) error { name := file.name if root.matchesOFD("Res") { for _, group := range root.children { @@ -199,7 +252,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) if slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject"}, node.name.Local) { object = node.attr("ID") } - if node.matchesOFD("ImageObject") { + if retain && node.matchesOFD("ImageObject") { catalog.images = append(catalog.images, editorFontText{file: file, node: node}) } add := func(role, id string) { @@ -227,7 +280,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) return nil } return visit(root, "") - }) + }, retain) if err != nil { return nil, err }