diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index b5a94ac..9660148 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -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") { diff --git a/ofdgo_editor_xml.go b/ofdgo_editor_xml.go index 85163fc..b40dddd 100644 --- a/ofdgo_editor_xml.go +++ b/ofdgo_editor_xml.go @@ -42,22 +42,40 @@ type editorXMLPatch struct { // editorXMLReader 在内存XML上直接读取字节,按块检查长词元的取消信号 type editorXMLReader struct { - *bytes.Reader - ctx context.Context - remaining uint16 + data []byte + offset int + ctx context.Context } // 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 diff --git a/ofdgo_resource.go b/ofdgo_resource.go index 837ad45..ca86899 100644 --- a/ofdgo_resource.go +++ b/ofdgo_resource.go @@ -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 标识冲突