feat(资源管理): 优化资源首次扫描
build.yaml / build (push) Successful in 50s

This commit is contained in:
xiaoqidun committed 2026-10-15 17:47:03 +08:00
1 parent 59e3a7d36e
commit 6926ceafc9
3 files changed
+170 -11

No files matched your search

+17 -3
View File
@@ -67,13 +67,14 @@ 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)
return scanEditorFontFilesMode(ctx, reader, index, inspect, true, nil)
}
// scanEditorFontFilesMode 按需保留文字节点及原文,只读访问在扫描后释放非资源文件原文
// 入参: ctx 取消上下文, reader 包快照, index 文档索引, inspect 可选的文件节点访问器, retain 是否保留编辑数据
// stream 为可选的只读访问器,依次返回是否已处理、是否不安全、依赖路径及错误
// 返回: *editorFontDocument 扫描结果, error 读取、结构或访问错误
func scanEditorFontFilesMode(ctx context.Context, reader *Reader, index int, inspect func(*editorFontXML, *editorXML) error, retain bool) (*editorFontDocument, error) {
func scanEditorFontFilesMode(ctx context.Context, reader *Reader, index int, inspect func(*editorFontXML, *editorXML) error, retain bool, stream func(*editorFontXML) (bool, bool, []string, error)) (*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 {
@@ -93,11 +94,24 @@ func scanEditorFontFilesMode(ctx context.Context, reader *Reader, index int, ins
if err != nil {
return nil, err
}
file := &editorFontXML{name: name, data: data}
if !retain && stream != nil {
handled, unsafe, links, err := stream(file)
if err != nil {
return nil, err
}
if handled {
scan.files[name] = file
scan.unsafe = scan.unsafe || unsafe
queue = append(queue, links...)
file.data = nil
continue
}
}
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
file := &editorFontXML{name: name, data: data}
scan.files[name] = file
base := ""
if root.matchesOFD("Res") {
+25 -7
View File
@@ -42,22 +42,40 @@ type editorXMLPatch struct {
// editorXMLReader 在内存XML上直接读取字节,按块检查长词元的取消信号
type editorXMLReader struct {
*bytes.Reader
data []byte
offset int
ctx context.Context
remaining uint16
}
// ReadByte 读取XML词元字节,每4096字节检查取消状态
// 返回: byte 当前字节, error 取消或读取错误
func (r *editorXMLReader) ReadByte() (byte, error) {
if r.remaining == 0 {
if r.offset&4095 == 0 {
if err := r.ctx.Err(); err != nil {
return 0, err
}
r.remaining = 4096
}
r.remaining--
return r.Reader.ReadByte()
if r.offset >= len(r.data) {
return 0, io.EOF
}
b := r.data[r.offset]
r.offset++
return b, nil
}
// Read 读取XML原文字节并检查取消状态
// 入参: p 接收缓冲区
// 返回: int 已读字节数, error 取消或读取错误
func (r *editorXMLReader) Read(p []byte) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
if r.offset >= len(r.data) {
return 0, io.EOF
}
n := copy(p, r.data[r.offset:])
r.offset += n
return n, nil
}
// editorXMLPrefix 跳过UTF-8字节序标记和XML空白,探测后保留首个内容字节
@@ -106,7 +124,7 @@ func parseEditorXML(data []byte) (*editorXML, error) {
func parseEditorXMLContext(ctx context.Context, data []byte) (*editorXML, error) {
var input io.Reader = bytes.NewReader(data)
if ctx != nil {
input = &editorXMLReader{Reader: bytes.NewReader(data), ctx: ctx}
input = &editorXMLReader{data: data, ctx: ctx}
}
d := xml.NewDecoder(input)
var root *editorXML
+128 -1
View File
@@ -17,6 +17,7 @@ package ofdgo
import (
"cmp"
"context"
"encoding/xml"
"fmt"
"io"
"path"
@@ -64,6 +65,15 @@ type resourceCatalog struct {
images []editorFontText
}
// resourcePageFrame 保存流式页面扫描的图元归属及直接文本位置
type resourcePageFrame struct {
name string
object string
start int
role string
link bool
}
// Resources 列出当前文档及所选版本的资源,标记缺失文件,不读取二进制内容或执行媒体动作
// 重复查询复用已完成的扫描,返回值及引用位置与阅读器相互独立
// 入参: ctx 取消上下文
@@ -280,7 +290,9 @@ func (r *Reader) resourceCatalogMode(ctx context.Context, retain bool) (*resourc
return nil
}
return visit(root, "")
}, retain)
}, retain, func(file *editorFontXML) (bool, bool, []string, error) {
return scanResourcePage(ctx, file, pages, refs)
})
if err != nil {
return nil, err
}
@@ -327,6 +339,121 @@ func (r *Reader) resourceCatalogMode(ctx context.Context, retain bool) (*resourc
return catalog, ctx.Err()
}
// scanResourcePage 流式收集常规页面引用,特殊结构回退到完整节点扫描
// 入参: ctx 取消上下文, file 页面原文, pages 文件页码, refs 已收集引用
// 返回: bool 是否已处理, bool 引用是否不安全, []string 依赖路径, error 解析或取消错误
func scanResourcePage(ctx context.Context, file *editorFontXML, pages map[string]int, refs map[string][]ResourceReference) (bool, bool, []string, error) {
d := xml.NewDecoder(&editorXMLReader{data: file.data, ctx: ctx})
var stack []resourcePageFrame
var links []string
local := make(map[string][]ResourceReference)
page, exists := pages[file.name]
if !exists {
page = -1
}
seen, unsafe := false, false
add := func(role, id, object string) {
key := editorResourceID(id)
if role == "AttachID" {
key = "Attachment/" + id
}
if key != "" {
local[key] = append(local[key], ResourceReference{File: file.name, Page: page, Object: object, Role: role})
}
}
for {
if err := ctx.Err(); err != nil {
return true, false, nil, err
}
start := int(d.InputOffset())
token, err := d.Token()
if err == io.EOF {
if !seen {
return false, false, nil, nil
}
for key, values := range local {
refs[key] = append(refs[key], values...)
}
return true, unsafe, links, nil
}
if err != nil {
return true, false, nil, err
}
switch token := token.(type) {
case xml.StartElement:
if len(stack) == 0 {
if seen {
return true, false, nil, fmt.Errorf("multiple XML roots")
}
if token.Name.Local != "Page" {
return false, false, nil, nil
}
seen = true
}
if classifyOFDNamespace(token.Name.Space) == ofdXMLUnknown || slices.Contains([]string{"Attachments", "Signatures", "CustomTags", "Extensions", "ExtendData", "Data"}, token.Name.Local) {
return false, false, nil, nil
}
for _, attr := range token.Attr {
if attr.Name.Space != "" && attr.Name.Space != "xmlns" && attr.Name.Space != "http://www.w3.org/XML/1998/namespace" {
return false, false, nil, nil
}
}
frame := resourcePageFrame{name: token.Name.Local, start: start}
parent := ""
if len(stack) != 0 {
outer := stack[len(stack)-1]
if outer.role != "" || outer.link {
return false, false, nil, nil
}
frame.object, parent = outer.object, outer.name
}
node := editorXML{name: token.Name, attrs: token.Attr}
if slices.Contains([]string{"TextObject", "PathObject", "ImageObject", "CompositeObject"}, frame.name) {
frame.object = node.attr("ID")
}
if frame.name == "TextObject" || frame.name == "Text" && parent == "Area" {
unsafe = unsafe || editorResourceID(node.attr("Font")) == ""
}
for _, attr := range token.Attr {
if attr.Name.Space == "" && (editorResourceReferenceKind(attr.Name.Local, false) == editorReferenceID || attr.Name.Local == "AttachID") {
add(attr.Name.Local, attr.Value, frame.object)
}
}
if slices.Contains([]string{"Thumbnail", "Substitution", "DefaultCS"}, frame.name) {
frame.role = frame.name
}
value := ""
switch frame.name {
case "PublicRes", "DocumentRes", "PageRes", "Annotations":
frame.link = true
case "Page", "TemplatePage", "CompositeGraphicUnit":
value = node.attr("BaseLoc")
case "PageAnnot":
value = node.attr("FileLoc")
case "DrawParam":
value = node.attr("Link")
}
if value != "" {
links = append(links, resolveResourcePath(file.name, "", value))
}
stack = append(stack, frame)
case xml.EndElement:
frame := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if frame.role != "" || frame.link {
node := editorXML{start: frame.start, end: int(d.InputOffset())}
value := strings.TrimSpace(editorImportText(file.data, &node))
if frame.role != "" {
add(frame.role, value, frame.object)
}
if frame.link && value != "" {
links = append(links, resolveResourcePath(file.name, "", value))
}
}
}
}
}
// add 加入资源声明,拒绝同一类型内的重复标识
// 入参: item 资源信息, node 声明位置
// 返回: error 标识冲突