diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index b858281..d431fd0 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1245,7 +1245,11 @@ el.structureForm.addEventListener("submit", async event => { if (!state.editing || document.body.hasAttribute("aria-busy")) return; const index = Number(el.structureEntry.value), path = JSON.parse(el.structureNodeSelect.value || "[]"); const action = index < 0 ? "create" : el.structureAction.value; - const node = { name: el.structureName.value, namespace: el.structureNamespace.value, text: el.structureText.value, + let original = state.doc.structures?.[index]?.root; + for (const part of path) original = original?.children?.[part]; + const text = action === "update" && original?.text?.replace(/\r\n?/g, "\n") === el.structureText.value + ? original.text : el.structureText.value; + const node = { name: el.structureName.value, namespace: el.structureNamespace.value, text, attributes: [...el.structureAttributes.children].map(row => ({ name: row.querySelector('[name="name"]').value, namespace: row.querySelector('[name="namespace"]').value, value: row.querySelector('[name="value"]').value })) }; const references = [...el.structureReferences.children].map(row => ({ pageId: row.querySelector('[name="page"]').value, objectIds: row.querySelector('[name="ids"]').value.split(/[,\s]+/u).filter(Boolean) })).filter(ref => ref.objectIds.length); @@ -8443,7 +8447,7 @@ function structureNode(node, index, path) { content.className = "structure-content"; details.append(content); for (const attr of node.attributes || []) appendInfoLine(content, attr.name, attr.value); - if (node.text && !node.references?.length) appendInfoLine(content, "原文", node.text); + if (node.text?.trim() && !node.references?.length) appendInfoLine(content, "原文", node.text); const children = node.children || []; const references = node.references?.length || children.some(child => child.references?.length); for (const [i, child] of children.entries()) { diff --git a/ofdgo_compression_fonts.go b/ofdgo_compression_fonts.go index 35d33bd..64a9ba8 100644 --- a/ofdgo_compression_fonts.go +++ b/ofdgo_compression_fonts.go @@ -77,7 +77,10 @@ func resourceFontUsage(ctx context.Context, resources []string, scoped map[strin alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)) id, charset := editorResourceID(font.attr("ID")), font.attr("Charset") unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate - for _, path := range []string{location, alternate} { + for i, path := range [2]string{location, alternate} { + if i == 1 && path == location { + continue + } if result[path] == nil { result[path] = newEditorFontUsage() } diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index b023cf2..e027837 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -32,7 +32,7 @@ type compressionImages struct { patternVisits map[compressionPatternKey]bool } -// compressionImagePlan 为有损输出分文档分析图片,按共享路径合并最大尺寸和保护条件 +// compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理 // 入参: ctx 取消上下文, options 压缩配置 // 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误 func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOptions) (map[string]bool, map[string]image.Point, error) { @@ -42,10 +42,16 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp images := make(map[string]bool) sizes := make(map[string]image.Point) blocked := make(map[string]bool) - for index := 0; index < r.DocumentCount(); index++ { + isolated := r.OFD.DocBody[r.documentIndex].versioned + current := make(map[string]bool) + for offset := 0; offset < r.DocumentCount(); offset++ { if err := ctx.Err(); err != nil { return nil, nil, err } + index := offset + if isolated { + index = (r.documentIndex + offset) % r.DocumentCount() + } document := r if index != r.DocumentIndex() { var err error @@ -80,6 +86,13 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp } for _, img := range catalog { name := cleanPackagePath(img.Location) + if isolated { + if index == r.documentIndex { + current[name] = true + } else if current[name] { + continue + } + } if old, ok := images[name]; !ok || old { images[name] = !masks[name] } diff --git a/ofdgo_compression_parallel.go b/ofdgo_compression_parallel.go index 91024d0..ca929fb 100644 --- a/ofdgo_compression_parallel.go +++ b/ofdgo_compression_parallel.go @@ -126,25 +126,59 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e } // outputImageWorkingSet 按原图尺寸估算解码和重采样缓冲,不以降采样目标代替原图 -// 按两份16位RGBA像素缓冲分批,不代表实际内存上限;格式或尺寸不明时单独处理 +// 通常预留两份16位RGBA缓冲,TIFF另计分量、瓦片及蒙版存储;格式或尺寸不明时单独处理 // 入参: ctx 取消上下文, data 原始图像编码 // 返回: int 编码及像素缓冲的估算字节数,上限为单批预算 func outputImageWorkingSet(ctx context.Context, data []byte) int { if len(data) >= outputImageWorkingSetLimit { return outputImageWorkingSetLimit } - input := imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx} - config, _, err := decodeImageConfig(input) + input := bytes.NewReader(data) + available := outputImageWorkingSetLimit - len(data) + if isTIFFData(data) { + layout, err := readTIFFLayoutContext(ctx, input) + if err != nil { + return outputImageWorkingSetLimit + } + return len(data) + tiffImageWorkingSet(layout, available) + } + config, _, err := decodeImageConfig(imageInput{ReadCloser: io.NopCloser(input), context: ctx}) if err != nil || config.Width <= 0 || config.Height <= 0 { return outputImageWorkingSetLimit } - available := outputImageWorkingSetLimit - len(data) if config.Width > available/16/config.Height { return outputImageWorkingSetLimit } return len(data) + config.Width*config.Height*16 } +// tiffImageWorkingSet 分别估算显示缓冲及主图、蒙版的完整瓦片存储,超预算时返回上限 +// 入参: layout 已校验的主图布局, limit 可用字节数 +// 返回: int 不超过limit的估算值 +func tiffImageWorkingSet(layout tiffImageLayout, limit int) int { + size := layout.displaySize() + if size.X <= 0 || size.Y <= 0 || size.X > limit/16/size.Y { + return limit + } + cost := size.X * size.Y * 16 + for source := &layout; source != nil; source = source.transparency { + width, height := uint64(source.width), uint64(source.height) + if source.tileWidth != 0 { + x, y := uint64(source.tileWidth), uint64(source.tileHeight) + width, height = ((width-1)/x+1)*x, ((height-1)/y+1)*y + } + pixelSize := max(uint64(16), source.decodedPixelBytes()) + if !source.planar { + pixelSize = max(pixelSize, uint64(source.samples)*4) + } + if width > uint64(limit-cost)/pixelSize/height { + return limit + } + cost += int(width * height * pixelSize) + } + return cost +} + // runOutputImages 等待整批结束后记录更小候选,失败图片保留原始编码 // 入参: jobs 独占图片任务, results 本次写出的候选缓存 // 返回: error 取消错误 diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index eea40d2..da5ae30 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -50,7 +50,7 @@ func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, } // compressResourceReferences 优化资源,复用自产页面引用并流式读取暂存页面 -// 图片优化结果仅在本次写出内复用,失败时保留原始编码 +// 版本文件隔离当前入口,其他文档仅优化未受版本保护的资源;结果仅在本次写出内复用 // 入参: parts 输出改动, reader 原包, removed 已移除条目, generated 本次自产页面及引用 // 返回: error 读取或取消错误 func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Reader, removed map[string]bool, generated map[string]editorGeneratedReferences) error { @@ -58,7 +58,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea return nil } generated = e.generatedVectorReferences(parts, generated) - scope, err := reader.versionResourceFiles(e.output.ctx, parts, generated) + selected, scope, err := reader.versionResourceFiles(e.output.ctx, parts, generated) if err != nil { return err } @@ -107,7 +107,22 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea if err != nil || !safe { return err } + if selected != nil && reader.DocumentCount() > 1 { + preserved, err := reader.versionPreservedFiles(e.output.ctx) + if err != nil { + return err + } + for name := range preserved { + if !selected[name] { + refs.files[name], refs.versions[name] = true, true + } + } + } type resourceFile struct{ key, role string } + type resourceScope struct { + role string + selected bool + } type resourceSize struct { role string size uint64 @@ -147,6 +162,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea return err } key := editorResourceLocation(name, root.attr("BaseLoc"), value) + if selected != nil && !selected[cleanPackagePath(name)] && selected[key] { + continue + } if preserved || field == "MediaFile" && node.attr("Type") != "Image" { refs.files[key] = true continue @@ -174,7 +192,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea reserved[key] = true } formats := make(map[string]string) - seen := make(map[string]map[[32]byte]string) + seen := make(map[resourceScope]map[[32]byte]string) imageResults := make(map[outputImageKey][]byte) imageSources := make(map[string]outputImageSource) ordered := slices.Sorted(maps.Keys(actual)) @@ -321,18 +339,19 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea } } } - if seen[role] == nil { - seen[role] = make(map[[32]byte]string) + scope := resourceScope{role, selected[key]} + if seen[scope] == nil { + seen[scope] = make(map[[32]byte]string) } hash := source.key.hash - if first, ok := seen[role][hash]; ok { + if first, ok := seen[scope][hash]; ok { aliases[key] = first removed[key] = true if locations[key] != actual[key] { delete(parts, locations[key]) } } else { - seen[role][hash] = key + seen[scope][hash] = key } } } @@ -367,6 +386,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea return err } key := editorResourceLocation(name, root.attr("BaseLoc"), value) + if selected != nil && !selected[cleanPackagePath(name)] && selected[key] { + continue + } location := locations[key] if first, ok := aliases[key]; ok { location = locations[first] diff --git a/ofdgo_compression_tiff.go b/ofdgo_compression_tiff.go index 340bd82..f473f34 100644 --- a/ofdgo_compression_tiff.go +++ b/ofdgo_compression_tiff.go @@ -23,20 +23,21 @@ import ( ) // optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 +// 主图或单个瓦片的预计缓冲超限时保留原编码 // 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 // 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) { - layout, err := readTIFFLayout(bytes.NewReader(data)) + layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return data, ctx.Err() } - pixelSize := int64(8) - if layout.planar { - pixelSize = 16 - } + pixelSize := int64(layout.decodedPixelBytes()) if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) { return data, ctx.Err() } + if layout.tileWidth != 0 && int64(layout.tileWidth) > outputOptimizationBufferLimit/pixelSize/int64(layout.tileHeight) { + return data, ctx.Err() + } density, ok := tiffCompressionDensity(data, layout) if !ok { return data, ctx.Err() @@ -73,7 +74,11 @@ func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity, value = uint32(order.Uint16(entry[8:])) } switch tag { - case 256, 257, 258, 259, 262, 273, 274, 277, 278, 279, 320, 322, 323, 324, 325: + case 256, 257, 258, 259, 273, 274, 277, 278, 279, 320, 322, 323, 324, 325: + case 262: + if value == 5 { + return density, false + } case 254: if value != 0 { return density, false diff --git a/ofdgo_editor.go b/ofdgo_editor.go index ce42acc..ed401ed 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -66,6 +66,7 @@ type Editor struct { source *editorSource origins map[string]*editorObjectOrigin removedPages map[string]bool + structureRemaps map[string][]map[string]string outlines []byte encryption *encryptionState output *outputOptimization @@ -82,6 +83,7 @@ type editorResource struct { composite string vectorKey [32]byte vectorStates map[string]editorCompositeState + objectIDs map[string]string references []string usage *editorVectorUsage subset *editorFontSubset @@ -120,9 +122,7 @@ func (e *Editor) SetFontFS(fsys ...fs.FS) { // 入参: compiler 页面编译器 func (e *Editor) SetPageCompiler(compiler PageCompiler) { e.backends.Compiler = compiler - if e.fontRenderer != nil { - WithRenderBackends(e.backends)(e.fontRenderer) - } + e.fontRenderer = nil } // SetRenderBackends 设置完整后端组合,字体能力变化时更新度量,不改变文档或撤销历史 @@ -130,15 +130,10 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) { func (e *Editor) SetRenderBackends(backends RenderBackends) { fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts) if !sameBackend(e.backends.FontResources, backends.FontResources) { - e.fonts = make(map[string]*FontResource) - for i := range e.resources { - e.resources[i].subset = nil - } + e.clearFontResourceCache() } e.backends = backends - if e.fontRenderer != nil { - WithRenderBackends(backends)(e.fontRenderer) - } + e.fontRenderer = nil if fontsChanged { e.fontMetrics = make(map[string]FontMetrics) } diff --git a/ofdgo_editor_annot.go b/ofdgo_editor_annot.go index a76a8f4..2e25a70 100644 --- a/ofdgo_editor_annot.go +++ b/ofdgo_editor_annot.go @@ -152,10 +152,11 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi } wanted := make(map[string]bool, len(ids)) for _, id := range ids { - if id == "" || wanted[id] { + key := editorResourceID(id) + if key == "" || wanted[key] { return fmt.Errorf("missing or duplicate annotation ID %q", id) } - wanted[id] = true + wanted[key] = true } if len(wanted) == 0 { return nil @@ -212,7 +213,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi continue } count++ - id := node.attr("ID") + id := editorResourceID(node.attr("ID")) if !wanted[id] { continue } @@ -262,7 +263,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi parts[file] = editorPatchXML(content, changes) } for _, id := range ids { - if !found[id] { + if !found[editorResourceID(id)] { return fmt.Errorf("annotation %q not found", id) } } @@ -290,5 +291,9 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi if len(parts) == 0 { return nil } - return e.commitAnnotationParts(e.source, parts) + base, err := e.annotationStructureSource(index, wanted) + if err != nil { + return err + } + return e.commitAnnotationParts(base, parts) } diff --git a/ofdgo_editor_annot_content.go b/ofdgo_editor_annot_content.go index 636fed0..eb75810 100644 --- a/ofdgo_editor_annot_content.go +++ b/ofdgo_editor_annot_content.go @@ -213,13 +213,17 @@ func (e *Editor) AnnotationTexts(index int, id string) ([]string, error) { return texts, nil } -// annotationXML 读取唯一注解的独立原文,不以类型、大小或可见性筛选 +// annotationXML 按ST_ID数值读取唯一注解的原文,不以类型、大小或可见性筛选 // 入参: index 页面索引, id 注解标识 // 返回: []byte 注解XML, error 错误信息 func (e *Editor) annotationXML(index int, id string) ([]byte, error) { if index < 0 || index >= len(e.pages) { return nil, fmt.Errorf("page index %d out of range", index) } + key := editorResourceID(id) + if key == "" { + return nil, fmt.Errorf("invalid annotation ID %q", id) + } if e.source == nil || e.source.document.Annotations == "" { return nil, fmt.Errorf("page has no annotations") } @@ -252,7 +256,7 @@ func (e *Editor) annotationXML(index int, id string) ([]byte, error) { return nil, err } for _, node := range page.children { - if !node.matchesOFD("Annot") || node.attr("ID") != id { + if !node.matchesOFD("Annot") || editorResourceID(node.attr("ID")) != key { continue } if result != nil { @@ -521,7 +525,7 @@ func (e *Editor) annotationScope(page int, path ObjectPath) (*Reader, *Renderer, if err != nil { return nil, nil, nil, nil, err } - root.setStates(e.source.annotationStates[e.pages[page].ID+"/"+path.Annotation]) + root.setStates(e.source.annotationStates[e.pages[page].ID+"/"+editorResourceID(path.Annotation)]) reader, err := e.Reader() if err != nil { return nil, nil, nil, nil, err @@ -578,7 +582,7 @@ func (e *Editor) writeAnnotationScope(page int, id string, data []byte, states m if statesByID == nil { statesByID = make(map[string]map[string]editorCompositeState) } - statesByID[e.pages[page].ID+"/"+id] = maps.Clone(states) + statesByID[e.pages[page].ID+"/"+editorResourceID(id)] = maps.Clone(states) e.source.annotationStates = statesByID } return err diff --git a/ofdgo_editor_annot_ops.go b/ofdgo_editor_annot_ops.go index df0b9c6..6fbec85 100644 --- a/ofdgo_editor_annot_ops.go +++ b/ofdgo_editor_annot_ops.go @@ -42,10 +42,11 @@ func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelectio selection := &AnnotationSelection{editor: e, document: e.documentIdentity(), states: make(map[string]map[string]editorCompositeState)} seen := make(map[string]bool) for _, id := range ids { - if seen[id] { + key := editorResourceID(id) + if seen[key] { return nil, fmt.Errorf("duplicate annotation ID %q", id) } - seen[id] = true + seen[key] = true data, err := e.annotationXML(page, id) if err != nil { return nil, err @@ -58,7 +59,7 @@ func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelectio return nil, fmt.Errorf("annotation contains unsupported identifiers") } selection.data = append(selection.data, data) - selection.states[id] = maps.Clone(e.source.annotationStates[e.pages[page].ID+"/"+id]) + selection.states[key] = maps.Clone(e.source.annotationStates[e.pages[page].ID+"/"+key]) } if e.originalPage(page) { origin := e.source.pages[e.pages[page].ID] @@ -106,7 +107,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx, return err } id := ids[editorResourceID(root.attr("ID"))] - states[edit.pages[page].ID+"/"+id] = remapCompositeStates(selection.states[root.attr("ID")], ids) + states[edit.pages[page].ID+"/"+id] = remapCompositeStates(selection.states[editorResourceID(root.attr("ID"))], ids) data, err = editorXMLRemapIDs(data, root, ids) if err != nil { return err diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 76d510a..9adee9e 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -70,6 +70,7 @@ type editorCompositeNode struct { patches []editorXMLPatch index int states map[string]editorCompositeState + remaps []map[string]string } // editorCompositeState 保存内部对象的会话排版及可还原裁剪,不写入OFD @@ -828,7 +829,9 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render return err } if path.Annotation != "" { - return e.writeAnnotationScope(page, path.Annotation, data, root.states) + return e.remapStructureObjects(page, root.remaps, func() error { + return e.writeAnnotationScope(page, path.Annotation, data, root.states) + }) } next, err := newEditorCompositeNode(data) if err != nil { @@ -842,7 +845,9 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render next.node.parent = origin.node.parent updated.page = origin.page } - return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated}) + return e.remapStructureObjects(page, root.remaps, func() error { + return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated}) + }) } // transformCompositeMember 将页面变换换算至成员坐标,保留边框及旧式边界约定 @@ -983,6 +988,7 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er } if changed { patches = append(patches, editorXMLPatch{child.span.start, child.span.end, data}) + node.remaps = append(node.remaps, child.remaps...) removeCompositeStates(states, child.node) maps.Copy(states, child.states) } @@ -996,10 +1002,12 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er return nil, false, err } if dirty { + node.remaps = append(node.remaps, node.ref.remaps...) id, err := e.addCompositeResource(fragment, node.ref.states) if err != nil { return nil, false, err } + node.remaps = append(node.remaps, e.resources[len(e.resources)-1].objectIDs) root, err := parseEditorXML(data) if err != nil { return nil, false, err @@ -1049,6 +1057,7 @@ func (e *Editor) addCompositeResource(data []byte, states map[string]editorCompo return "", err } resource.states = remapCompositeStates(states, ids) + resource.objectIDs = ids e.resources = append(e.resources, resource) return id, nil } diff --git a/ofdgo_editor_composite_copy.go b/ofdgo_editor_composite_copy.go index 7392644..333a96c 100644 --- a/ofdgo_editor_composite_copy.go +++ b/ofdgo_editor_composite_copy.go @@ -47,7 +47,7 @@ func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection return nil, err } var result []string - err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error { + err := e.pasteCompositeSelection(selection, func(objects []GraphicObject, _ []map[string]string) error { var err error result, err = e.CopyObjects(page, objects, dx, dy) return err @@ -124,7 +124,7 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com } if e.pages[page].ID != selection.page || path.ID != selection.path.ID || path.Annotation != selection.path.Annotation || !slices.Equal(path.Children, selection.path.Children) { var result []int - err := e.pasteCompositeSelection(selection, func(objects []GraphicObject) error { + err := e.pasteCompositeSelection(selection, func(objects []GraphicObject, _ []map[string]string) error { var err error result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy) return err @@ -198,7 +198,7 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com // pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源 // 入参: selection 独立内部快照, paste 目标范围粘贴操作 // 返回: error 错误信息 -func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste func([]GraphicObject) error) (err error) { +func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste func([]GraphicObject, []map[string]string) error) (err error) { if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) { return fmt.Errorf("composite selection belongs to another document or editor") } @@ -223,6 +223,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu defer reader.Close() renderer := e.newRenderer(reader) var objects []GraphicObject + var remaps []map[string]string for _, source := range selection.nodes { copy, err := newEditorCompositeNode(source.data) if err != nil { @@ -327,8 +328,9 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu } object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object} objects = append(objects, object) + remaps = append(remaps, copy.remaps...) } - return paste(objects) + return paste(objects, remaps) } // preserveActionRegions 将默认点击范围固定到对象坐标,保留动作原文及显式区域 diff --git a/ofdgo_editor_composite_group.go b/ofdgo_editor_composite_group.go index 66fecef..45883c5 100644 --- a/ofdgo_editor_composite_group.go +++ b/ofdgo_editor_composite_group.go @@ -63,6 +63,7 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int) if err != nil { return err } + nodes[0].owner.remaps = append(nodes[0].owner.remaps, group.remaps...) for i, node := range nodes { var replacement []byte if i == 0 { @@ -119,9 +120,11 @@ func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode hash.Write(content) key := [32]byte(hash.Sum(nil)) var resource string + var ids map[string]string for _, candidate := range e.resources { if candidate.vectorKey == key && maps.Equal(candidate.vectorStates, states) { resource = candidate.composite + ids = candidate.objectIDs break } } @@ -136,12 +139,17 @@ func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode } added := &e.resources[len(e.resources)-1] added.vectorKey, added.vectorStates = key, maps.Clone(states) + ids = added.objectIDs } data, err := editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: editorBoxString(extent)}, {Name: xml.Name{Local: "ResourceID"}, Value: resource}}, nil) if err != nil { return nil, err } - return newEditorCompositeNode(data) + group, err := newEditorCompositeNode(data) + if err == nil { + group.remaps = []map[string]string{ids} + } + return group, err } // UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪 @@ -154,7 +162,8 @@ func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([ if !members[0].Capabilities.Order { return fmt.Errorf("composite container cannot be reordered") } - return e.ungroupCompositeNode(renderer, node, func(objects []GraphicObject) error { + return e.ungroupCompositeNode(renderer, node, func(objects []GraphicObject, remaps []map[string]string) error { + node.owner.remaps = append(node.owner.remaps, remaps...) var content []byte for i, object := range objects { content = append(content, object.origin.data...) @@ -173,7 +182,7 @@ func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([ // ungroupCompositeNode 将组合展开至所在坐标系,祖先透明度和裁剪仅由原父级应用一次 // 入参: renderer 当前读取器, node 待展开组合, replace 原子替换回调 // 返回: error 错误信息 -func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeNode, replace func([]GraphicObject) error) error { +func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeNode, replace func([]GraphicObject, []map[string]string) error) error { if !e.compositeUngroupable(node.node) { return fmt.Errorf("group container cannot be removed without losing attributes") } @@ -201,7 +210,7 @@ func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeN return fmt.Errorf("group member cannot be expanded") } } - return e.pasteCompositeSelection(&CompositeSelection{editor: e, document: e.documentIdentity(), nodes: members}, func(objects []GraphicObject) error { + return e.pasteCompositeSelection(&CompositeSelection{editor: e, document: e.documentIdentity(), nodes: members}, func(objects []GraphicObject, remaps []map[string]string) error { ids := make(map[string]string) if copy.ref != nil { for _, object := range objects { @@ -247,7 +256,13 @@ func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeN member.object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), data: member.data, node: member.node, object: member.object} objects[i] = member.object } - return replace(objects) + if len(ids) != 0 { + remaps = append(remaps, ids) + } + for _, object := range objects { + remaps = append(remaps, map[string]string{editorResourceID(editorObjectID(node.object)): editorResourceID(editorObjectID(object))}) + } + return replace(objects, remaps) }) } diff --git a/ofdgo_editor_composite_insert.go b/ofdgo_editor_composite_insert.go index 41274bf..8e03cef 100644 --- a/ofdgo_editor_composite_insert.go +++ b/ofdgo_editor_composite_insert.go @@ -299,6 +299,7 @@ func (e *Editor) isolateCompositeDefaultPaint(node *editorCompositeNode) error { if err != nil { return err } + node.remaps = append(node.remaps, e.resources[len(e.resources)-1].objectIDs) } cache[key] = replacement } diff --git a/ofdgo_editor_documents.go b/ofdgo_editor_documents.go index 027054f..b6d240a 100644 --- a/ofdgo_editor_documents.go +++ b/ofdgo_editor_documents.go @@ -430,6 +430,17 @@ func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error if e.source != nil && e.documentRoot() == reader.documentRoot && current == version && e.Info.DocID == reader.OFD.DocBody[reader.documentIndex].DocInfo.DocID { source := *e.source source.reader = reader + if len(e.structureRemaps) != 0 { + tags, err := reader.CustomTagsContext(ctx) + if err != nil { + return err + } + source.structureStarts = make(map[int]map[string]int, len(tags)) + starts := e.structureRemapStarts() + for index := range tags { + source.structureStarts[index] = starts + } + } e.source = &source } else { next, err := reader.EditorContext(ctx) diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index 227fdf5..ab3c746 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -49,6 +49,14 @@ type editorSourceFontKey struct { // editorSourceFontCache 在单次资源处理中复用裁剪结果,空值表示无体积收益 type editorSourceFontCache map[editorSourceFontKey][]byte +// clearFontResourceCache 清除字体后端生成的解析与裁剪缓存,保留原始资源 +func (e *Editor) clearFontResourceCache() { + e.fonts = make(map[string]*FontResource) + for i := range e.resources { + e.resources[i].subset = nil + } +} + // subset 按原字体和用字复用安全子集,不缓存错误、取消结果或原始字体 // 入参: ctx 取消上下文, backend 字体资源后端, data 原字体, usage 用字记录 // 返回: []byte 更小的字体子集,无收益时为nil, error 裁剪或取消错误 diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 7ec848e..04e3487 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -647,6 +647,9 @@ func (e *Editor) restoreFontSnapshot(state Editor) { state = state.transactionSnapshot() state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit state.serial, state.revision = e.serial, e.revision + if !sameBackend(state.backends.FontResources, e.backends.FontResources) { + state.clearFontResourceCache() + } state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress state.fontRenderer, state.fontSourcesCache = nil, nil diff --git a/ofdgo_editor_font_cleanup.go b/ofdgo_editor_font_cleanup.go index d5abb3f..a2da546 100644 --- a/ofdgo_editor_font_cleanup.go +++ b/ofdgo_editor_font_cleanup.go @@ -17,6 +17,7 @@ package ofdgo import ( "bytes" "context" + "encoding/binary" "fmt" "io" "maps" @@ -83,21 +84,12 @@ func (e *Editor) compactPackageFonts(ctx context.Context, parts map[string][]byt continue } seen[name] = true - input, err := view.openFile(name) + candidate, err := view.fontCleanupCandidate(ctx, name) if err != nil { return err } - var header [4]byte - n, err := io.ReadFull(input, header[:]) - input.Close() - if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { - return err - } - if n == len(header) { - switch string(header[:]) { - case "\x00\x01\x00\x00", "OTTO", "ttcf": - candidates[name] = true - } + if candidate { + candidates[name] = true } } } @@ -122,6 +114,91 @@ func (e *Editor) compactPackageFonts(ctx context.Context, parts map[string][]byt return nil } +// fontCleanupCandidate 检查字体目录与文件范围,仅将可识别字体交给引用清理流程 +// 入参: ctx 取消上下文, name 包内文件路径 +// 返回: bool 是否为候选字体, error 读取或取消错误 +func (r *Reader) fontCleanupCandidate(ctx context.Context, name string) (bool, error) { + input, err := r.openFile(name) + if err != nil { + return false, err + } + defer input.Close() + var size uint64 + if data, ok := r.files[cleanPackagePath(name)]; ok { + size = uint64(len(data)) + } else if file, ok := r.packageFile(name); ok { + size = file.UncompressedSize64 + } + candidate, err := fontCleanupDirectory(imageInput{ReadCloser: input, context: ctx}, size) + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = nil + } + if canceled := ctx.Err(); canceled != nil { + return false, canceled + } + return candidate, err +} + +// fontCleanupDirectory 检查SFNT表目录或字体集合索引,不读取字形数据 +// 入参: input 文件流, size 文件长度 +// 返回: bool 是否具有有效目录, error 读取错误 +func fontCleanupDirectory(input io.Reader, size uint64) (bool, error) { + var header [12]byte + if _, err := io.ReadFull(input, header[:]); err != nil { + return false, err + } + if size < uint64(len(header)) { + return false, nil + } + if string(header[:4]) == "ttcf" { + version := binary.BigEndian.Uint32(header[4:]) + count := uint64(binary.BigEndian.Uint32(header[8:])) + if version != 0x00010000 && version != 0x00020000 || count == 0 || count > (size-12)/4 { + return false, nil + } + end := 12 + count*4 + if version == 0x00020000 { + end += 12 + } + var entry [4]byte + for range count { + if _, err := io.ReadFull(input, entry[:]); err != nil { + return false, err + } + offset := uint64(binary.BigEndian.Uint32(entry[:])) + if offset < end || offset > size-12 { + return false, nil + } + } + return true, nil + } + switch string(header[:4]) { + case "\x00\x01\x00\x00", "OTTO", "true": + default: + return false, nil + } + count := uint64(binary.BigEndian.Uint16(header[4:])) + if count == 0 || count > (size-12)/16 { + return false, nil + } + end := 12 + count*16 + seen := make(map[uint32]bool, count) + var entry [16]byte + for range count { + if _, err := io.ReadFull(input, entry[:]); err != nil { + return false, err + } + tag := binary.BigEndian.Uint32(entry[:]) + offset := uint64(binary.BigEndian.Uint32(entry[8:])) + length := uint64(binary.BigEndian.Uint32(entry[12:])) + if seen[tag] || offset < end || offset > size || length > size-offset { + return false, nil + } + seen[tag] = true + } + return true, nil +} + // subsetPackageFonts 按文档作用域收集用字,合并共享字体需求并保留字形编号 // 入参: ctx 取消上下文, view 独立输出快照, removed 待删除条目, generated 本次编码的页面引用 // 返回: error 读取、裁剪或取消错误 diff --git a/ofdgo_editor_group.go b/ofdgo_editor_group.go index edc8904..a3a8f3e 100644 --- a/ofdgo_editor_group.go +++ b/ofdgo_editor_group.go @@ -165,7 +165,7 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) { } defer reader.Close() var result []string - err = e.ungroupCompositeNode(renderer, root, func(objects []GraphicObject) error { + err = e.ungroupCompositeNode(renderer, root, func(objects []GraphicObject, remaps []map[string]string) error { origins := make(map[string]*editorObjectOrigin, len(objects)) for i := range objects { object := &objects[i] @@ -176,8 +176,10 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) { origins[id] = © result = append(result, id) } - e.replaceGroup(page, indexes, objects, origins) - return nil + return e.remapStructureObjects(page, remaps, func() error { + e.replaceGroup(page, indexes, objects, origins) + return nil + }) }) return result, err } diff --git a/ofdgo_editor_history.go b/ofdgo_editor_history.go index e982296..ed461b4 100644 --- a/ofdgo_editor_history.go +++ b/ofdgo_editor_history.go @@ -29,6 +29,7 @@ type editorChange struct { // Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史 // 回调仅操作传入的编辑器,不可保留该实例供后续使用 +// 后端、字体来源和撤销配置仅用于本次回调,不随事务提交 // 入参: edit 批量修改回调 // 返回: error 修改错误 func (e *Editor) Transaction(edit func(*Editor) error) error { @@ -48,9 +49,12 @@ func (e *Editor) Transaction(edit func(*Editor) error) error { } changed := e.revision != before.revision e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit + if !sameBackend(e.backends.FontResources, before.backends.FontResources) { + e.clearFontResourceCache() + } e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS e.fontSourcesCache = before.fontSourcesCache - if e.sameDocument(&before) { + if e.sameResourceView(&before) { e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics } else { e.fontRenderer, e.fontMetrics = nil, make(map[string]FontMetrics) @@ -140,6 +144,7 @@ func (e *Editor) transactionSnapshot() Editor { next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID) next.origins = maps.Clone(e.origins) next.removedPages = maps.Clone(e.removedPages) + next.structureRemaps = maps.Clone(e.structureRemaps) next.fontMetrics = maps.Clone(e.fontMetrics) if e.source != nil { source := *e.source @@ -149,16 +154,19 @@ func (e *Editor) transactionSnapshot() Editor { return next } -// restoreTransaction 恢复文档状态,保留历史和配置,仅在同一文档内共享资源 +// restoreTransaction 恢复文档状态,保留历史和配置,仅在同一资源视图内共享缓存 // 入参: state 文档状态 func (e *Editor) restoreTransaction(state Editor) { state = state.transactionSnapshot() state.retainDocumentResources(e) - if !e.sameDocument(&state) { + if !e.sameResourceView(&state) { state.fontRenderer, state.fontMetrics = nil, make(map[string]FontMetrics) } state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit state.serial = max(e.serial, state.serial) + if !sameBackend(state.backends.FontResources, e.backends.FontResources) { + state.clearFontResourceCache() + } state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS state.fontSourcesCache = e.fontSourcesCache state.validatedPaths = e.validatedPaths @@ -166,10 +174,26 @@ func (e *Editor) restoreTransaction(state Editor) { *e = state } -// retainDocumentResources 保留同一文档后续注册的资源,不跨文档混用编号和字体缓存 +// sameResourceView 判断文档和资源来源是否一致,新建文档首次封装不改变已注册资源 +// 入参: other 编辑快照 +// 返回: bool 是否可共享注册资源和字体缓存 +func (e *Editor) sameResourceView(other *Editor) bool { + if !e.sameDocument(other) { + return false + } + if e.source == nil { + return other.source == nil || other.source.fromNew + } + if other.source == nil { + return e.source.fromNew + } + return e.source.reader == other.source.reader +} + +// retainDocumentResources 保留同一资源视图中后续注册的资源,不跨包快照混用编号和字体缓存 // 入参: source 当前文档状态 func (e *Editor) retainDocumentResources(source *Editor) { - if !e.sameDocument(source) { + if !e.sameResourceView(source) { return } if len(source.resources) > len(e.resources) { @@ -182,7 +206,7 @@ func (e *Editor) retainDocumentResources(source *Editor) { e.fontRenderer, e.fontMetrics = source.fontRenderer, source.fontMetrics } -// recordTransaction 记录事务快照,跨文档撤销前保留各文档后续注册的资源 +// recordTransaction 记录事务快照,在各自资源视图中保留后续注册的资源 // 入参: before 修改前的文档状态 func (e *Editor) recordTransaction(before Editor) { if change := e.recordChange(); change != nil { diff --git a/ofdgo_editor_images.go b/ofdgo_editor_images.go index 828eeab..0359eba 100644 --- a/ofdgo_editor_images.go +++ b/ofdgo_editor_images.go @@ -114,7 +114,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag if err := ctx.Err(); err != nil { return 0, err } - config, format, err := decodeImageConfigData(data) + config, format, err := decodeImageConfigDataContext(ctx, data) if err != nil { return 0, err } @@ -133,7 +133,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag if err != nil { return 0, err } - metadata, err := imagePageMetadata(data, format, options.DPI) + metadata, err := imagePageMetadataContext(ctx, data, format, options.DPI) if err != nil { return 0, err } diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 25078bf..e792377 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -307,7 +307,7 @@ func (e *Editor) importPages(ctx context.Context, source *Reader, indexes []int, if at < 0 || at > len(e.pages) { return nil, fmt.Errorf("page index %d out of range", at) } - doc, err := source.Doc() + doc, err := source.DocContext(ctx) if err != nil { return nil, err } @@ -1795,7 +1795,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s if err := reader.initRoot(); err != nil { return nil, err } - doc, err := reader.Doc() + doc, err := reader.DocContext(m.ctx) if err != nil { return nil, err } diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 3990eef..73a5168 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -15,6 +15,7 @@ package ofdgo import ( + "bufio" "bytes" "context" "encoding/xml" @@ -45,6 +46,8 @@ type editorSource struct { annotationPages map[string]bool retiredAttachments map[string]bool removedAttachments map[string]bool + structureObjects map[string]map[string]bool + structureStarts map[int]map[string]int } // editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用 @@ -298,9 +301,6 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) { } maximum := e.maxID for name := range names { - if !strings.EqualFold(path.Ext(name), ".xml") { - continue - } input, err := reader.openFile(name) if err != nil { return 0, err @@ -315,11 +315,35 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) { return maximum, nil } -// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件 -// 入参: input XML输入流 +// editorXMLMaxID 按内容识别OFD结构并读取最大标识,跳过二进制及非OFD内容 +// 入参: input 包内文件流 // 返回: int 最大标识, error 错误信息 func editorXMLMaxID(input io.Reader) (int, error) { - decoder := xml.NewDecoder(input) + buffer := bufio.NewReader(input) + header, err := buffer.Peek(3) + if err != nil && err != io.EOF { + return 0, err + } + if bytes.Equal(header, []byte{0xef, 0xbb, 0xbf}) { + buffer.Discard(3) + } + for { + header, err := buffer.Peek(1) + if err == io.EOF { + return 0, nil + } + if err != nil { + return 0, err + } + if !strings.ContainsRune(" \t\r\n", rune(header[0])) { + if header[0] != '<' { + return 0, nil + } + break + } + buffer.Discard(1) + } + decoder := xml.NewDecoder(buffer) maximum, started := 0, false for { token, err := decoder.Token() @@ -344,7 +368,7 @@ func editorXMLMaxID(input io.Reader) (int, error) { } for _, attr := range node.Attr { if attr.Name.Space == "" && attr.Name.Local == "ID" { - id, err := strconv.Atoi(attr.Value) + id, err := strconv.Atoi(editorResourceID(attr.Value)) if err == nil { maximum = max(maximum, id) } diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index 892522b..03628db 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -76,9 +76,13 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri } } else { name := e.packageName(packagePagePath("", page.ID)) + bound := len(source.structureObjects[editorResourceID(page.ID)]) != 0 var scan *editorReferenceScan if generated != nil { scan = &editorReferenceScan{refs: &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}, name: name, safe: true} + if bound { + scan.graphicIDs = make(map[string]bool) + } } if stage && generated != nil && !e.sourcePagePreserved(page) { data, err := stager.encode(e, page, scan) @@ -89,9 +93,10 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri known := editorGeneratedReferences{staged: data} if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { known.refs = scan.refs + known.objects = scan.graphicIDs } generated[name] = known - if (reader.DocumentCount() > 1 || reader.OFD.DocBody[reader.documentIndex].versioned) && !known.stagedLeaf(nil) { + if (reader.DocumentCount() > 1 || reader.OFD.DocBody[reader.documentIndex].versioned) && !known.stagedLeaf(nil) || bound && known.objects == nil { input := data.open() if e.output != nil { input = imageInput{ReadCloser: input, context: e.output.ctx} @@ -101,7 +106,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri if err != nil { return nil, err } - generated[name] = editorGeneratedReferences{data: parts[name], refs: known.refs} + generated[name] = editorGeneratedReferences{data: parts[name], refs: known.refs, objects: known.objects} } } else { data, err := e.sourcePageData(page, scan) @@ -110,7 +115,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri } parts[name] = data if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { - generated[name] = editorGeneratedReferences{data: data, refs: scan.refs} + generated[name] = editorGeneratedReferences{data: data, refs: scan.refs, objects: scan.graphicIDs} } } pageRefs[i] = Page{ID: page.ID, BaseLoc: "/" + name} @@ -272,7 +277,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri return nil, err } } - if err := e.pruneStructureReferences(parts); err != nil { + if err := e.pruneStructureReferences(parts, generated); err != nil { return nil, err } for name, data := range maps.Clone(parts) { diff --git a/ofdgo_editor_reference.go b/ofdgo_editor_reference.go index 3a086ab..b241856 100644 --- a/ofdgo_editor_reference.go +++ b/ofdgo_editor_reference.go @@ -26,11 +26,12 @@ import ( // editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容 const editorVectorUsageLimit = 4096 -// editorGeneratedReferences 保存本次输出条目及其引用,不跨写出复用 +// editorGeneratedReferences 保存本次输出条目、资源引用及完整图元编号,不跨写出复用 type editorGeneratedReferences struct { - data []byte - refs *editorResourceRefs - staged *editorStagedPage + data []byte + refs *editorResourceRefs + staged *editorStagedPage + objects map[string]bool } // editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效 @@ -40,19 +41,20 @@ type editorVectorUsage struct { refs *editorResourceRefs } -// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 +// editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描 type editorReferenceScan struct { - refs *editorResourceRefs - objects map[string]bool - name string - base string - stack []string - fonts []*editorFontUsage - text strings.Builder - resource bool - capture bool - safe bool - seen bool + refs *editorResourceRefs + objects map[string]bool + graphicIDs map[string]bool + name string + base string + stack []string + fonts []*editorFontUsage + text strings.Builder + resource bool + capture bool + safe bool + seen bool } // matches 判断条目仍为本次编码结果,空条目与暂存占位不混用 @@ -159,6 +161,10 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { } } var usage *editorFontUsage + switch token.Name.Local { + case "Template", "PageRes", "CompositeObject", "CompositeGraphicUnit", "PageBlock": + s.graphicIDs = nil + } if len(s.fonts) != 0 { usage = s.fonts[len(s.fonts)-1] if usage != nil && (s.stack[len(s.stack)-1] == "TextCode" || s.stack[len(s.stack)-1] == "Glyphs") { @@ -175,6 +181,13 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { } if attr.Name.Local != "ID" { s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource) + } else if s.graphicIDs != nil { + switch token.Name.Local { + case "TextObject", "PathObject", "ImageObject": + if id := editorResourceID(attr.Value); id != "" { + s.graphicIDs[id] = true + } + } } root, parent := token.Name.Local, "" if len(s.stack) != 0 { diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index e1d4f48..edaf835 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -74,7 +74,7 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read ctx = e.output.ctx } progress = progress.withContext(ctx) - scope, err := reader.versionResourceFiles(ctx, parts, generated) + _, scope, err := reader.versionResourceFiles(ctx, parts, generated) if err != nil { return nil, err } @@ -351,16 +351,16 @@ func (r *editorResourceRefs) reference(name, base, key, value string, resource b if kind == editorReferenceNone { return } - value = strings.TrimSpace(value) - if value == "" { - return - } if kind == editorReferenceID { if id := editorResourceID(value); id != "" { r.ids[id] = true } return } + value = strings.TrimSpace(value) + if value == "" { + return + } r.files[editorResourceLocation(name, base, value)] = true r.files[cleanPackagePath(resolveResourcePath(name, base, value))] = true } @@ -389,7 +389,7 @@ func editorResourceReferenceKind(key string, resource bool) int { // 入参: value 标识文本 // 返回: string 标准十进制标识,无效时为空 func editorResourceID(value string) string { - id, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32) + id, err := strconv.ParseUint(strings.Trim(value, " \t\r\n"), 10, 32) if err != nil || id == 0 { return "" } diff --git a/ofdgo_editor_structure.go b/ofdgo_editor_structure.go index b56a49e..c814c1c 100644 --- a/ofdgo_editor_structure.go +++ b/ofdgo_editor_structure.go @@ -19,15 +19,17 @@ import ( "context" "encoding/xml" "fmt" + "maps" "path" + "slices" "strings" ) -// pruneStructureReferences 清理本次编辑删除的公文图元引用,未知标引和原有悬空引用保持原样 +// pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用 // 改动进入输出快照,由既有文档及版本隔离流程维护共享文件 -// 入参: parts 输出覆盖条目 +// 入参: parts 输出覆盖条目, generated 本次编码结果及扫描记录 // 返回: error 读取、解析或取消错误 -func (e *Editor) pruneStructureReferences(parts map[string][]byte) error { +func (e *Editor) pruneStructureReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error { if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 { return nil } @@ -35,43 +37,144 @@ func (e *Editor) pruneStructureReferences(parts map[string][]byte) error { if e.output != nil { ctx = e.output.ctx } + tags, err := e.source.reader.CustomTagsContext(ctx) + if err != nil { + return err + } + if !slices.ContainsFunc(tags, func(tag CustomTag) bool { + return structureOfficial(tag, tag.NameSpace) + }) { + return nil + } pages := make(map[string]bool, len(e.pages)) for _, page := range e.pages { pages[editorResourceID(page.ID)] = true } removedPages := make(map[string]bool) removedObjects := make(map[string]map[string]bool) + pageRemaps := make(map[string]map[string][]string) + remainingObjects := make(map[string]map[string]bool) + var snapshot *Reader + var snapshotPages map[string]Page + sources := make(map[string]*editorSourcePage, len(e.source.pages)+len(e.source.structureObjects)) for id, source := range e.source.pages { - key := editorResourceID(id) + sources[editorResourceID(id)] = source + } + for id := range e.source.structureObjects { + if _, ok := sources[id]; !ok { + sources[id] = nil + } + } + for key, source := range sources { if !pages[key] { removedPages[key] = true continue } - data, changed := parts[e.source.reader.ResPath(source.ref.BaseLoc)] - if !changed || source.original == nil { + known := e.source.structureObjects[key] + changed := false + if source != nil { + _, changed = parts[e.source.reader.ResPath(source.ref.BaseLoc)] + changed = changed && source.original != nil + } + if !changed && len(known) == 0 { continue } - root, err := parseEditorXMLContext(ctx, data) + if snapshot == nil { + reader := e.source.reader + files := make(map[string][]byte, len(reader.files)+len(parts)) + maps.Copy(files, reader.files) + maps.Copy(files, parts) + snapshot = &Reader{Zip: reader.Zip, files: files, encryption: e.encryption, documentIndex: reader.documentIndex, selectedVersion: reader.selectedVersion} + if err := snapshot.initRootContext(ctx); err != nil { + return err + } + doc, err := snapshot.docStructureContext(ctx) + if err != nil { + return err + } + snapshotPages = make(map[string]Page, len(doc.Pages.Page)) + for _, page := range doc.Pages.Page { + snapshotPages[editorResourceID(page.ID)] = page + } + } + original := make(map[string]bool) + if source != nil { + var err error + original, err = structurePageObjectIDs(ctx, e.source.reader, source.ref) + if err != nil { + return err + } + } + maps.Copy(original, known) + page := snapshotPages[key] + name := snapshot.ResPath(page.BaseLoc) + scanned := generated[name] + remaining := scanned.objects + if remaining == nil || !scanned.matches(parts[name]) { + var err error + remaining, err = structurePageObjectIDs(ctx, snapshot, page) + if err != nil { + return err + } + } else if snapshot.doc.Annotations != "" { + if _, err := snapshot.DocContext(ctx); err != nil { + return err + } + visitor := &structureObjectVisitor{objects: maps.Clone(remaining)} + renderer := NewRenderer(snapshot) + renderer.pageContext = ctx + for _, annotation := range snapshot.Annots[page.ID] { + if err := renderer.walkAnnotation(annotation, visitor); err != nil { + return err + } + } + remaining = visitor.objects + } + remainingObjects[key] = remaining + links, err := structureRemapLinks(ctx, e.structureRemaps[key]) if err != nil { return err } - remaining := make(map[string]bool) - var scan func(*editorXML) - scan = func(node *editorXML) { - if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown { - return - } - if id := editorResourceID(node.attr("ID")); id != "" { - remaining[id] = true - } - for _, child := range node.children { - scan(child) + related := make(map[string][]string) + for from, targets := range links { + for _, to := range targets { + if err := ctx.Err(); err != nil { + return err + } + related[from] = append(related[from], to) + related[to] = append(related[to], from) } } - scan(root) - for id := range source.nodes { - id = editorResourceID(id) - if id != "" && !remaining[id] { + for id := range remaining { + if len(related[id]) != 0 { + original[id] = true + } + } + pending := slices.Collect(maps.Keys(original)) + for len(pending) != 0 { + if err := ctx.Err(); err != nil { + return err + } + id := pending[len(pending)-1] + pending = pending[:len(pending)-1] + for _, next := range related[id] { + if !original[next] { + original[next] = true + pending = append(pending, next) + } + } + } + for id := range original { + if err := ctx.Err(); err != nil { + return err + } + if len(links[id]) != 0 { + if pageRemaps[key] == nil { + pageRemaps[key] = make(map[string][]string) + } + pageRemaps[key][id] = links[id] + } + if !remaining[id] { if removedObjects[key] == nil { removedObjects[key] = make(map[string]bool) } @@ -79,52 +182,281 @@ func (e *Editor) pruneStructureReferences(parts map[string][]byte) error { } } } - if len(removedPages) == 0 && len(removedObjects) == 0 { + if len(removedPages) == 0 && len(removedObjects) == 0 && len(pageRemaps) == 0 { return nil } - tags, err := e.source.reader.CustomTags() - if err != nil { - return err - } + protected := make(map[string]bool) for _, tag := range tags { + if !structureOfficial(tag, tag.NameSpace) { + protected[e.source.reader.ResPath(tag.FileLoc)] = true + } + } + isolated := make(map[string]string) + originals := make(map[string][]byte) + redirects := make(map[int]string) + for index, tag := range tags { if !structureOfficial(tag, tag.NameSpace) { continue } name := e.source.reader.ResPath(tag.FileLoc) - data, ok := parts[name] + data, ok := originals[name] if !ok { - data, err = e.source.reader.readFileViewContext(ctx, name) - if err != nil { - return err + data, ok = parts[name] + if !ok { + data, err = e.source.reader.readFileViewContext(ctx, name) + if err != nil { + return err + } + } + originals[name] = data + } + remaps := pageRemaps + if starts := e.source.structureStarts[index]; len(starts) != 0 { + remaps = maps.Clone(pageRemaps) + for page, start := range starts { + links, err := structureRemapLinks(ctx, e.structureRemaps[page][start:]) + if err != nil { + return err + } + remaps[page] = links } } - updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string) { + mappedObjects := make(map[string]map[string][]string) + updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string, error) { key := editorResourceID(page) if removedPages[key] { - return page, nil + return page, nil, nil } kept := make([]string, 0, len(ids)) for _, id := range ids { - if !removedObjects[key][editorResourceID(id)] { + canonical := editorResourceID(id) + if len(remaps[key][canonical]) != 0 { + mapped, ok := mappedObjects[key][canonical] + if !ok { + var err error + mapped, err = structureMappedIDs(ctx, canonical, remaps[key], remainingObjects[key]) + if err != nil { + return "", nil, err + } + if mappedObjects[key] == nil { + mappedObjects[key] = make(map[string][]string) + } + mappedObjects[key][canonical] = mapped + } + kept = append(kept, mapped...) + } else if !removedObjects[key][canonical] { kept = append(kept, id) } } - return page, kept + return page, kept, nil }) if err != nil { return err } if !bytes.Equal(data, updated) { + if protected[name] { + target := isolated[name] + if target == "" { + target = packageAvailableName(e.source.reader, parts, path.Join(e.packageDirectory(), "Tags", "Tag.xml")) + isolated[name] = target + } + redirects[index] = target + name = target + } parts[name] = updated } } + if len(redirects) != 0 { + return e.redirectStructureEntries(parts, redirects) + } return nil } +// redirectStructureEntries 一次改写输出快照中的标引路径,保留同次页面及索引改动 +// 入参: parts 输出覆盖条目, targets 标引入口序号及新内容路径 +// 返回: error 索引读取或路径改写错误 +func (e *Editor) redirectStructureEntries(parts map[string][]byte, targets map[int]string) error { + name, data, _, entries, err := editorStructureIndex(e.source, parts) + if err != nil { + return err + } + patches := make([]editorXMLPatch, 0, len(targets)) + for index, target := range targets { + if index < 0 || index >= len(entries) { + return fmt.Errorf("custom tag structure not found: %d", index) + } + loc := entries[index].child("FileLoc") + if loc == nil { + return fmt.Errorf("custom tag requires FileLoc") + } + var value bytes.Buffer + if err := xml.EscapeText(&value, []byte("/"+target)); err != nil { + return err + } + patches = append(patches, editorXMLContent(data, loc, value.Bytes())) + } + parts[name] = editorPatchXML(data, patches) + return nil +} + +// remapStructureObjects 将图元修改和标引编号迁移作为一次操作提交,映射只作用于当前页 +// 入参: page 页面索引, remaps 按修改顺序记录的编号映射, edit 图元修改 +// 返回: error 图元修改错误 +func (e *Editor) remapStructureObjects(page int, remaps []map[string]string, edit func() error) error { + if len(remaps) == 0 || e.source == nil || e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 { + return edit() + } + return e.Transaction(func(editor *Editor) error { + if err := edit(); err != nil { + return err + } + if editor.structureRemaps == nil { + editor.structureRemaps = make(map[string][]map[string]string) + } + key := editorResourceID(editor.pages[page].ID) + next := slices.Clone(editor.structureRemaps[key]) + for _, ids := range remaps { + if len(ids) != 0 { + next = append(next, maps.Clone(ids)) + } + } + editor.structureRemaps[key] = next + return nil + }) +} + +// structureRemapStarts 记录各页面已应用的编号迁移位置,不与后续编辑共享可变状态 +// 返回: map[string]int 页面编号及已应用的迁移记录数 +func (e *Editor) structureRemapStarts() map[string]int { + starts := make(map[string]int, len(e.structureRemaps)) + for page, remaps := range e.structureRemaps { + starts[page] = len(remaps) + } + return starts +} + +// structureRemapLinks 合并按操作顺序记录的编号迁移,保留一对多关系并排除重复边 +// 入参: ctx 取消上下文, remaps 图元编号迁移记录 +// 返回: map[string][]string 原编号及其迁移目标, error 取消错误 +func structureRemapLinks(ctx context.Context, remaps []map[string]string) (map[string][]string, error) { + links := make(map[string][]string) + seen := make(map[[2]string]bool) + for _, ids := range remaps { + if err := ctx.Err(); err != nil { + return nil, err + } + for from, to := range ids { + if err := ctx.Err(); err != nil { + return nil, err + } + key := [2]string{from, to} + if from == to || seen[key] { + continue + } + seen[key] = true + links[from] = append(links[from], to) + } + } + return links, ctx.Err() +} + +// structureMappedIDs 沿编号迁移关系收集当前仍可达的图元,同一来源的结果不重复 +// 入参: ctx 取消上下文, id 原编号, links 编号迁移关系, remaining 当前页面可达图元 +// 返回: []string 保留的原编号及迁移后的编号, error 取消错误 +func structureMappedIDs(ctx context.Context, id string, links map[string][]string, remaining map[string]bool) ([]string, error) { + var result []string + pending := []string{id} + seen := make(map[string]bool) + for len(pending) != 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + id = pending[len(pending)-1] + pending = pending[:len(pending)-1] + if seen[id] { + continue + } + seen[id] = true + if remaining[id] { + result = append(result, id) + } + for i := len(links[id]) - 1; i >= 0; i-- { + pending = append(pending, links[id][i]) + } + } + return result, nil +} + +// structurePageObjectIDs 收集页面、模板、复合资源及注释外观中的图元标识,不加载字体或图像 +// 入参: ctx 取消上下文, reader 资源已加载的阅读器, page 页面引用 +// 返回: map[string]bool 可达图元标识, error 页面读取或遍历错误 +func structurePageObjectIDs(ctx context.Context, reader *Reader, page Page) (map[string]bool, error) { + if _, err := reader.DocContext(ctx); err != nil { + return nil, err + } + content, err := reader.PageContentContext(ctx, page) + if err != nil { + return nil, err + } + renderer := NewRenderer(reader, WithAnnotations(true)) + renderer.pageContext = ctx + visitor := &structureObjectVisitor{objects: make(map[string]bool)} + if err := renderer.WalkPage(content, visitor); err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + return visitor.objects, nil +} + +// annotationStructureSource 记录被修改注释原有的外观图元,供保存时清理失效标引 +// 入参: page 页面索引, wanted 待修改注释标识 +// 返回: *editorSource 保留已知图元的包快照, error 读取或遍历错误 +func (e *Editor) annotationStructureSource(page int, wanted map[string]bool) (*editorSource, error) { + base := e.source + if base.document.CustomTags.Path == "" && len(base.document.CustomTags.CustomTag) == 0 { + return base, nil + } + tags, err := base.reader.CustomTags() + if err != nil { + return nil, err + } + if !slices.ContainsFunc(tags, func(tag CustomTag) bool { return structureOfficial(tag, tag.NameSpace) }) { + return base, nil + } + if _, err := base.reader.Doc(); err != nil { + return nil, err + } + if source := base.pages[e.pages[page].ID]; source != nil { + if _, err := base.reader.PageContent(source.ref); err != nil { + return nil, err + } + } + visitor := &structureObjectVisitor{objects: make(map[string]bool)} + renderer := NewRenderer(base.reader) + for _, annotation := range base.reader.Annots[e.pages[page].ID] { + if wanted[editorResourceID(annotation.ID)] { + if err := renderer.walkAnnotation(annotation, visitor); err != nil { + return nil, err + } + } + } + next := *base + next.structureObjects = maps.Clone(base.structureObjects) + if next.structureObjects == nil { + next.structureObjects = make(map[string]map[string]bool) + } + key := editorResourceID(e.pages[page].ID) + maps.Copy(visitor.objects, next.structureObjects[key]) + next.structureObjects[key] = visitor.objects + return &next, nil +} + // rewriteStructureReferences 仅改写已识别公文命名空间的叶子引用,保留其他XML字节 // 入参: ctx 取消上下文, data 原始标引, tag 标引入口, rewrite 引用映射,空标识列表表示删除引用 // 返回: []byte 修改后的标引, error 解析或取消错误 -func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, rewrite func(string, []string) (string, []string)) ([]byte, error) { +func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, rewrite func(string, []string) (string, []string, error)) ([]byte, error) { root, err := parseEditorXMLContext(ctx, data) if err != nil { return nil, err @@ -152,7 +484,10 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, if len(ids) == 0 { return nil } - page, next := rewrite(node.attr("PageRef"), ids) + page, next, err := rewrite(node.attr("PageRef"), ids) + if err != nil { + return err + } if len(next) == 0 { patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) return nil @@ -190,7 +525,7 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, // structures 迁移所选页面的公文标引,按已迁移图元映射引用,不猜测其他行业的引用规则 // 返回: []byte 标引索引条目, error 读取、解析或取消错误 func (m *editorPageImport) structures() ([]byte, error) { - tags, err := m.reader.CustomTags() + tags, err := m.reader.CustomTagsContext(m.ctx) if err != nil { return nil, err } @@ -204,10 +539,10 @@ func (m *editorPageImport) structures() ([]byte, error) { return nil, err } count := 0 - updated, err := rewriteStructureReferences(m.ctx, data, tag, func(page string, ids []string) (string, []string) { + updated, err := rewriteStructureReferences(m.ctx, data, tag, func(page string, ids []string) (string, []string, error) { key := editorImportKey(page) if !m.pages[key] { - return page, nil + return page, nil, nil } var mapped []string for _, id := range ids { @@ -216,7 +551,7 @@ func (m *editorPageImport) structures() ([]byte, error) { } } count += len(mapped) - return m.ids[key], mapped + return m.ids[key], mapped, nil }) if err != nil { return nil, err diff --git a/ofdgo_editor_structure_content.go b/ofdgo_editor_structure_content.go index 3083010..fbdf1c9 100644 --- a/ofdgo_editor_structure_content.go +++ b/ofdgo_editor_structure_content.go @@ -19,6 +19,7 @@ import ( "context" "encoding/xml" "fmt" + "maps" "slices" "strings" ) @@ -53,7 +54,8 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) { if err != nil { return 0, err } - if err := e.validateStructureReferences(tree, tag); err != nil { + objects, err := e.validateStructureReferences(tree, tag, nil) + if err != nil { return 0, err } base := e.source @@ -70,7 +72,7 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) { } tag.SchemaLoc = "/" + name } - name, index, container, entries, err := editorStructureIndex(base) + name, index, container, entries, err := editorStructureIndex(base, nil) if err != nil { return 0, err } @@ -107,7 +109,7 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) { return 0, err } } - return len(entries), e.commitAnnotationParts(base, parts) + return len(entries), e.commitStructureParts(base, parts, objects, len(entries)) } // DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源 @@ -117,7 +119,7 @@ func (e *Editor) DeleteStructure(index int) error { if e.source == nil { return fmt.Errorf("custom tag structure not found: %d", index) } - name, data, _, entries, err := editorStructureIndex(e.source) + name, data, _, entries, err := editorStructureIndex(e.source, nil) if err != nil { return err } @@ -125,7 +127,16 @@ func (e *Editor) DeleteStructure(index int) error { return fmt.Errorf("custom tag structure not found: %d", index) } node := entries[index] - return e.commitAnnotationParts(e.source, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}})}) + base := *e.source + base.structureStarts = make(map[int]map[string]int, len(e.source.structureStarts)) + for entry, starts := range e.source.structureStarts { + if entry < index { + base.structureStarts[entry] = starts + } else if entry > index { + base.structureStarts[entry-1] = starts + } + } + return e.commitAnnotationParts(&base, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}})}) } // AddStructureNode 在指定父节点末尾添加子树,空路径表示根节点 @@ -242,16 +253,20 @@ func (e *Editor) SetStructureReferences(index int, path []int, references []Stru } // editorStructureIndex 读取或准备当前文档的标引索引,保留内联入口和未知扩展 -// 入参: base 编辑包快照 +// 入参: base 编辑包快照, parts 本次输出条目,可为nil // 返回: string 索引文件路径, []byte 原文, *editorXML 容器, []*editorXML 标引入口, error 读取错误 -func editorStructureIndex(base *editorSource) (string, []byte, *editorXML, []*editorXML, error) { +func editorStructureIndex(base *editorSource, parts map[string][]byte) (string, []byte, *editorXML, []*editorXML, error) { name := base.reader.documentRoot if base.document.CustomTags.Path != "" { name = base.reader.ResPath(base.document.CustomTags.Path) } - data, err := base.reader.readFile(name) - if err != nil { - return "", nil, nil, nil, err + data, ok := parts[name] + if !ok { + var err error + data, err = base.reader.readFile(name) + if err != nil { + return "", nil, nil, nil, err + } } root, err := parseEditorXML(data) if err != nil { @@ -341,13 +356,14 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa if err != nil { return err } - if err := e.validateStructureReferences(tree, tag); err != nil { + objects, err := e.validateStructureReferences(tree, tag, snapshot) + if err != nil { return err } parts := make(map[string][]byte) for i, other := range tags { if i != index && e.source.reader.ResPath(other.FileLoc) == name { - indexName, indexData, _, entries, err := editorStructureIndex(e.source) + indexName, indexData, _, entries, err := editorStructureIndex(e.source, nil) if err != nil { return err } @@ -363,13 +379,38 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa } } parts[name] = updated - return e.commitAnnotationParts(e.source, parts) + return e.commitStructureParts(e.source, parts, objects, index) +} + +// commitStructureParts 提交标引并记录有效目标及已应用的编号迁移,供后续编辑维护引用 +// 入参: base 原包, parts 修改条目, objects 按页面编号记录的有效图元, index 标引入口序号 +// 返回: error 提交错误 +func (e *Editor) commitStructureParts(base *editorSource, parts map[string][]byte, objects map[string]map[string]bool, index int) error { + next := *base + if len(e.structureRemaps) != 0 { + next.structureStarts = maps.Clone(base.structureStarts) + if next.structureStarts == nil { + next.structureStarts = make(map[int]map[string]int) + } + next.structureStarts[index] = e.structureRemapStarts() + } + next.structureObjects = maps.Clone(base.structureObjects) + if len(objects) != 0 && next.structureObjects == nil { + next.structureObjects = make(map[string]map[string]bool) + } + for page, ids := range objects { + known := make(map[string]bool, len(next.structureObjects[page])+len(ids)) + maps.Copy(known, next.structureObjects[page]) + maps.Copy(known, ids) + next.structureObjects[page] = known + } + return e.commitAnnotationParts(&next, parts) } // validateStructureReferences 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码 -// 入参: root 待提交标引树, tag 标引入口 -// 返回: error 页面或图元缺失错误 -func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag) error { +// 入参: root 待提交标引树, tag 标引入口, r 当前操作的编辑快照,为nil时按需创建 +// 返回: map[string]map[string]bool 已校验目标, error 页面或图元缺失错误 +func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag, r *Reader) (map[string]map[string]bool, error) { var refs []StructureReference var collect func(*StructureNode, bool) error collect = func(node *StructureNode, official bool) error { @@ -386,46 +427,55 @@ func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag) return nil } if err := collect(root, structureOfficial(tag, root.Namespace)); err != nil { - return err + return nil, err } if len(refs) == 0 { - return nil + return nil, nil } - r, err := e.Reader() - if err != nil { - return err + if r == nil { + var err error + r, err = e.Reader() + if err != nil { + return nil, err + } + defer r.Close() } - defer r.Close() doc, err := r.Doc() if err != nil { - return err + return nil, err } renderer := e.newRenderer(r) pages := signaturePageNumbers(doc) checked := make(map[int]map[string]bool) + result := make(map[string]map[string]bool) for _, ref := range refs { page, _ := resourceValue(pages, ref.PageID) if page == 0 { - return fmt.Errorf("custom tag page not found: %s", ref.PageID) + return nil, fmt.Errorf("custom tag page not found: %s", ref.PageID) } objects := checked[page] if objects == nil { content, err := r.PageContentByIndex(page - 1) if err != nil { - return err + return nil, err } visitor := &structureObjectVisitor{objects: make(map[string]bool)} if err := renderer.WalkPage(content, visitor); err != nil { - return err + return nil, err } objects = visitor.objects checked[page] = objects } for _, id := range ref.ObjectIDs { if !objects[editorResourceID(id)] { - return fmt.Errorf("custom tag object not found: %s/%s", ref.PageID, id) + return nil, fmt.Errorf("custom tag object not found: %s/%s", ref.PageID, id) } + key := editorResourceID(ref.PageID) + if result[key] == nil { + result[key] = make(map[string]bool) + } + result[key][editorResourceID(id)] = true } } - return nil + return result, nil } diff --git a/ofdgo_image.go b/ofdgo_image.go index 82b14d9..47db175 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -156,14 +156,14 @@ func decodeImageDataContext(ctx context.Context, data []byte) (img image.Image, } }() if isJPEGData(data) || isPNGData(data) { - img, err := newEncodedImage(data) + img, err := newEncodedImageContext(ctx, data) if err != nil { return nil, "", err } return img, img.format, nil } if isTIFFData(data) { - layout, err := readTIFFLayout(bytes.NewReader(data)) + layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return nil, "tiff", err } @@ -201,28 +201,61 @@ func isPNGData(data []byte) bool { // 入参: data 图片数据 // 返回: image.Config 图片尺寸, string 图片格式, error 错误信息 func decodeImageConfigData(data []byte) (image.Config, string, error) { + return decodeImageConfigDataContext(context.Background(), data) +} + +// decodeImageConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态 +// 入参: ctx 取消上下文, data 图片数据 +// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误 +func decodeImageConfigDataContext(ctx context.Context, data []byte) (config image.Config, format string, err error) { + if err := ctx.Err(); err != nil { + return image.Config{}, "", err + } + defer func() { + if cancelErr := ctx.Err(); cancelErr != nil { + config, err = image.Config{}, cancelErr + } + }() if isTIFFData(data) { - layout, err := readTIFFLayout(bytes.NewReader(data)) + layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return image.Config{}, "tiff", err } - if layout.planar && layout.samples > 1 || layout.samples == 1 && (layout.depth == 2 || layout.depth == 4) { - img, _, err := tiffPlaneDirectories(nil, data, layout) + profile, err := readTIFFColorProfile(ctx, data, layout) + if err != nil { + return image.Config{}, "tiff", err + } + var cfg image.Config + if layout.needsSampleView() { + img, _, err := tiffPlaneDirectories(ctx, data, layout) + if err != nil { + return image.Config{}, "tiff", err + } + cfg.ColorModel = img.ColorModel() + } else { + input := newTIFFDirectoryInput(data, layout.offset) + input.ctx = ctx + cfg, err = tiff.DecodeConfig(input) if err != nil { return image.Config{}, "tiff", err } - size := layout.size() - return image.Config{ColorModel: img.ColorModel(), Width: size.X, Height: size.Y}, "tiff", nil } - cfg, err := tiff.DecodeConfig(newTIFFDirectoryInput(data, layout.offset)) - if err != nil { - return image.Config{}, "tiff", err + if profile != nil || layout.transparency != nil { + cfg.ColorModel = color.NRGBA64Model } - size := layout.size() + if layout.transparency != nil { + if _, err := readTIFFColorProfile(ctx, data, *layout.transparency); err != nil { + return image.Config{}, "tiff", err + } + if _, _, err := tiffPlaneDirectories(ctx, data, *layout.transparency); err != nil { + return image.Config{}, "tiff", err + } + } + size := layout.displaySize() cfg.Width, cfg.Height = size.X, size.Y return cfg, "tiff", nil } - cfg, format, err := image.DecodeConfig(bytes.NewReader(data)) + cfg, format, err := image.DecodeConfig(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) if err == nil { return cfg, normalizeSealType(format), nil } @@ -386,10 +419,12 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i if imageHighPrecision(source) { out := image.NewNRGBA64(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - if err := ctx.Err(); err != nil { - return nil, err - } for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } opacity := a if mask != nil { value, err := imageMaskAlpha(mask, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) @@ -419,10 +454,20 @@ func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (i copy(row, src.Pix[offset:offset+len(row)]) } else { for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } out.SetNRGBA(x, y, imageNRGBAAt(source, x, y)) } } for x := 0; x < bounds.Dx(); x++ { + if x&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } opacity := a if mask != nil { value, err := imageMaskAlpha(mask, maskBounds.Min.X+x, maskBounds.Min.Y+y-bounds.Min.Y) @@ -470,10 +515,12 @@ func validateImageMask(ctx context.Context, mask image.Image, size image.Point) } bounds := mask.Bounds() for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - if err := ctx.Err(); err != nil { - return err - } for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := ctx.Err(); err != nil { + return err + } + } if _, err := imageMaskAlpha(mask, x, y); err != nil { return err } @@ -541,13 +588,17 @@ func imageGrayAt(img image.Image, x, y int) color.Gray { return color.GrayModel.Convert(img.At(x, y)).(color.Gray) } -// imageNRGBAAt 获取图片NRGBA像素 +// imageNRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量 // 入参: img 图片对象, x X坐标, y Y坐标 // 返回: color.NRGBA NRGBA像素 func imageNRGBAAt(img image.Image, x, y int) color.NRGBA { - if src, ok := img.(*image.NRGBA); ok { + if src, ok := img.(interface{ NRGBAAt(int, int) color.NRGBA }); ok { return src.NRGBAAt(x, y) } + if src, ok := img.(interface{ NRGBA64At(int, int) color.NRGBA64 }); ok { + c := src.NRGBA64At(x, y) + return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)} + } if src, ok := img.(*image.YCbCr); ok { r, g, b, _ := src.YCbCrAt(x, y).RGBA() return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 255} @@ -565,7 +616,11 @@ func imageNRGBAAt(img image.Image, x, y int) color.NRGBA { } return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)} } - return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) + value := img.At(x, y) + if c, ok := value.(color.NRGBA64); ok { + return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)} + } + return color.NRGBAModel.Convert(value).(color.NRGBA) } // imageAlphaAt 读取十六位透明度,原生图像避免颜色装箱 @@ -579,15 +634,28 @@ func imageAlphaAt(img image.Image, x, y int) uint32 { return alpha } -// imageHighPrecision 判断颜色模型是否保留十六位采样 +// imageHighPrecision 判断颜色模型及调色板是否需要保留十六位采样 // 入参: img 图片对象 // 返回: bool 是否使用十六位颜色或透明度 func imageHighPrecision(img image.Image) bool { model := img.ColorModel() + if palette, ok := model.(color.Palette); ok { + for _, value := range palette { + switch value.(type) { + case color.RGBA, color.NRGBA, color.Gray, color.Alpha, color.CMYK, color.YCbCr, color.NYCbCrA: + default: + pixel := color.NRGBA64Model.Convert(value).(color.NRGBA64) + if pixel.R%257 != 0 || pixel.G%257 != 0 || pixel.B%257 != 0 || pixel.A%257 != 0 { + return true + } + } + } + return false + } return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model } -// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱 +// imageNRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱 // 入参: img 图片对象, x X坐标, y Y坐标 // 返回: color.NRGBA64 NRGBA64像素 func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { @@ -597,6 +665,9 @@ func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { case *image.NRGBA: c := src.NRGBAAt(x, y) return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257} + case *image.Gray: + v := uint16(src.GrayAt(x, y).Y) * 257 + return color.NRGBA64{R: v, G: v, B: v, A: 0xffff} case *image.Gray16: c := src.Gray16At(x, y) return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff} @@ -619,7 +690,11 @@ func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { } return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A} } - return color.NRGBA64Model.Convert(img.At(x, y)).(color.NRGBA64) + value := img.At(x, y) + if c, ok := value.(color.NRGBA); ok { + return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257} + } + return color.NRGBA64Model.Convert(value).(color.NRGBA64) } // transparentEdgeColor 获取透明像素相邻的可见颜色 @@ -731,6 +806,11 @@ func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image } offset := y*stride + step - 1 for x := 0; x < w; x++ { + if x&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, 0, err + } + } if (pixels[offset] == 0) != hasZero { hasZero, hasVisible = true, true break scan @@ -744,6 +824,11 @@ func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image return nil, 0, err } for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, 0, err + } + } if imageAlphaAt(source, x, y) == 0 { hasZero = true } else { diff --git a/ofdgo_image_bmp.go b/ofdgo_image_bmp.go index c76557c..176ede1 100644 --- a/ofdgo_image_bmp.go +++ b/ofdgo_image_bmp.go @@ -41,7 +41,7 @@ func decodeImageConfig(input io.Reader) (image.Config, string, error) { if err != nil { return image.Config{}, "tiff", err } - size := layout.size() + size := layout.displaySize() return image.Config{Width: size.X, Height: size.Y}, "tiff", nil } if len(prefix) == 34 && string(prefix[:2]) == "BM" { diff --git a/ofdgo_image_metadata.go b/ofdgo_image_metadata.go index ac3b55c..e13a1a4 100644 --- a/ofdgo_image_metadata.go +++ b/ofdgo_image_metadata.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "encoding/binary" "fmt" "hash/crc32" @@ -27,20 +28,27 @@ type imagePageInfo struct { xdpi, ydpi float64 } -// imagePageMetadata 读取图片方向与密度,TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF -// 入参: data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI -// 返回: imagePageInfo 方向与横纵DPI, error 元数据错误 -func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, error) { +// imagePageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消 +// TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF +// 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI +// 返回: imagePageInfo 方向与横纵DPI, error 元数据或取消错误 +func imagePageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (imagePageInfo, error) { info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi} + if err := ctx.Err(); err != nil { + return info, err + } var exif []byte var directory uint32 + scaleX, scaleY := 1.0, 1.0 if format == "tiff" { - layout, err := readTIFFLayout(bytes.NewReader(data)) + layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data)) if err != nil { return info, err } directory = layout.offset exif = data + original, display := layout.size(), layout.displaySize() + scaleX, scaleY = float64(display.X)/float64(original.X), float64(display.Y)/float64(original.Y) } if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 { x := int32(binary.LittleEndian.Uint32(data[38:42])) @@ -51,6 +59,9 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, } if format == "png" { for offset := 8; offset+12 <= len(data); { + if err := ctx.Err(); err != nil { + return info, err + } n := uint64(binary.BigEndian.Uint32(data[offset:])) if n > uint64(len(data)-offset-12) { return info, fmt.Errorf("truncated PNG chunk") @@ -81,6 +92,9 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, } } else if format == "jpeg" { for offset := 2; offset < len(data); { + if err := ctx.Err(); err != nil { + return info, err + } if data[offset] != 0xff { return info, fmt.Errorf("invalid JPEG marker") } @@ -140,7 +154,8 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, info.orientation = 1 } } - return info, nil + info.xdpi, info.ydpi = info.xdpi*scaleX, info.ydpi*scaleY + return info, ctx.Err() } // imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录 diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go index 9e1695a..2e87651 100644 --- a/ofdgo_image_resource.go +++ b/ofdgo_image_resource.go @@ -48,6 +48,13 @@ type encodedImageResult struct { err error } +// resampledImageMask 按替换图尺寸读取已校验的二值蒙版,不分配缩放像素 +type resampledImageMask struct { + source image.Image + original image.Rectangle + bounds image.Rectangle +} + // NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据 // 入参: data 原始编码 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误 @@ -236,30 +243,58 @@ func (r *Renderer) imageObjectMask(object ImageObject, target image.Image) (imag if err != nil { return nil, err } - bounds := mask.Bounds() - result := image.NewGray(image.Rectangle{Max: size}) - for y := range size.Y { - if err := ctx.Err(); err != nil { - return nil, err - } - sy := bounds.Min.Y + int((int64(y)*2+1)*int64(bounds.Dy())/(2*int64(size.Y))) - for x := range size.X { - sx := bounds.Min.X + int((int64(x)*2+1)*int64(bounds.Dx())/(2*int64(size.X))) - value, err := imageMaskAlpha(mask, sx, sy) - if err != nil { - return nil, err - } - result.SetGray(x, y, color.Gray{Y: value}) - } + return &resampledImageMask{source: mask, original: mask.Bounds(), bounds: image.Rectangle{Max: size}}, ctx.Err() +} + +// Bounds 返回替换图对应的蒙版范围 +// 返回: image.Rectangle 像素区域 +func (s *resampledImageMask) Bounds() image.Rectangle { return s.bounds } + +// ColorModel 返回二值蒙版的灰度颜色模型 +// 返回: color.Model 灰度模型 +func (s *resampledImageMask) ColorModel() color.Model { return color.GrayModel } + +// GrayAt 按像素中心读取原蒙版,区域外返回黑色 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.Gray 二值灰度 +func (s *resampledImageMask) GrayAt(x, y int) color.Gray { + if !image.Pt(x, y).In(s.bounds) { + return color.Gray{} } - return result, ctx.Err() + bounds, size := s.original, s.bounds.Size() + sx := bounds.Min.X + int((uint64(x-s.bounds.Min.X)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X)) + sy := bounds.Min.Y + int((uint64(y-s.bounds.Min.Y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y)) + return imageGrayAt(s.source, sx, sy) +} + +// At 返回采样后的二值灰度 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.Color 像素颜色 +func (s *resampledImageMask) At(x, y int) color.Color { return s.GrayAt(x, y) } + +// RGBA64At 返回不透明的十六位灰度颜色,供蒙版合成直接读取 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.RGBA64 像素颜色 +func (s *resampledImageMask) RGBA64At(x, y int) color.RGBA64 { + value := uint16(s.GrayAt(x, y).Y) * 257 + return color.RGBA64{R: value, G: value, B: value, A: 65535} } // newEncodedImage 接管编码数据并读取头信息 // 入参: data 原始编码 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误 func newEncodedImage(data []byte) (*EncodedImage, error) { - config, format, err := image.DecodeConfig(bytes.NewReader(data)) + return newEncodedImageContext(context.Background(), data) +} + +// newEncodedImageContext 读取PNG或JPEG头信息,元数据扫描期间响应取消,不复制输入或解码像素 +// 入参: ctx 取消上下文, data 只读图片编码 +// 返回: *EncodedImage 惰性图片, error 头信息、格式或取消错误 +func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, error) { + config, format, err := image.DecodeConfig(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) + if cancelErr := ctx.Err(); cancelErr != nil { + return nil, cancelErr + } if err != nil { return nil, err } @@ -291,7 +326,7 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { return img, nil } -// imageResourceCost 预留十六位像素空间,并计入编码、像素缓冲及补边源图的可见容量 +// imageResourceCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量 // 入参: img 图片资源, limit 缓存预算 // 返回: int 估算字节数或超预算标记 func imageResourceCost(img image.Image, limit int) int { @@ -307,7 +342,7 @@ func imageResourceCost(img image.Image, limit int) int { } return cost + pixels + cap(encoded.data) } - var buffers [4]int + var buffers [5]int switch source := img.(type) { case *image.RGBA: buffers[0] = cap(source.Pix) @@ -333,9 +368,47 @@ func imageResourceCost(img image.Image, limit int) int { } buffers[0], buffers[1] = cap(source.Pix), cap(source.Palette)*16 case *image.YCbCr: - buffers = [4]int{cap(source.Y), cap(source.Cb), cap(source.Cr)} + buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr)} case *image.NYCbCrA: - buffers = [4]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)} + buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)} + case *planarTIFFImage: + if cap(source.palette) > (limit-cost)/8 { + return limit + 1 + } + cost += cap(source.palette) * 8 + if cap(source.planes) > (limit-cost)/16 || len(source.planes) > len(buffers) { + return limit + 1 + } + cost += cap(source.planes) * 16 + for i, plane := range source.planes { + switch plane := plane.(type) { + case *image.Gray: + buffers[i] = cap(plane.Pix) + case *image.Gray16: + buffers[i] = cap(plane.Pix) + default: + buffers[i] = imageResourceCost(plane, limit-cost) + } + } + case *tiffFaxImage: + if cap(source.blocks) > (limit-cost)/16 { + return limit + 1 + } + cost += cap(source.blocks) * 16 + for _, block := range source.blocks { + size := imageResourceCost(block, limit-cost) + if size > limit-cost { + return limit + 1 + } + cost += size + } + case *orientedTIFFImage: + buffers[0] = imageResourceCost(source.source, limit-cost) + case *maskedTIFFImage: + buffers[0] = imageResourceCost(source.source, limit-cost) + buffers[1] = imageResourceCost(source.mask, limit-cost) + case *resampledImageMask: + buffers[0] = imageResourceCost(source.source, limit-cost) case *transparentEdgeImage: buffers[0] = imageResourceCost(source.source, limit-cost) case *transparentEdgeImage64: @@ -350,6 +423,9 @@ func imageResourceCost(img image.Image, limit int) int { } storage += size } + if pixels > limit-cost { + return limit + 1 + } return cost + max(pixels, storage) } diff --git a/ofdgo_image_tiff.go b/ofdgo_image_tiff.go index 4ed04cf..f2ab463 100644 --- a/ofdgo_image_tiff.go +++ b/ofdgo_image_tiff.go @@ -28,18 +28,26 @@ import ( // errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 var errTIFFRandomAccess = errors.New("TIFF directory requires random access") -// tiffImageLayout 保存单页TIFF的尺寸、方向、采样及压缩参数 +// tiffImageLayout 保存TIFF目录的图像布局及关联透明蒙版 type tiffImageLayout struct { width, height int + tileWidth int + tileHeight int orientation int offset, next uint32 reduced, mask bool planar bool + auxiliary bool samples uint32 depth uint32 fillOrder uint32 compression uint32 + predictor uint32 + photometric uint32 + profileOffset uint32 + profileSize uint32 t4, t6 uint32 + transparency *tiffImageLayout } // tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源 @@ -62,11 +70,21 @@ func isTIFFData(data []byte) bool { return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a") } -// readTIFFLayout 选择单页TIFF的主图,允许缩略图目录并拒绝循环和多个主图 +// readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图 // 入参: input 从文件头开始的输入流 // 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误 func readTIFFLayout(input io.Reader) (tiffImageLayout, error) { + return readTIFFLayoutContext(context.Background(), input) +} + +// readTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消 +// 入参: ctx 取消上下文, input 从文件头开始的输入流 +// 返回: tiffImageLayout 存储尺寸与方向, error 结构、读取或取消错误 +func readTIFFLayoutContext(ctx context.Context, input io.Reader) (tiffImageLayout, error) { var result tiffImageLayout + if err := ctx.Err(); err != nil { + return result, err + } var header [8]byte if _, err := io.ReadFull(input, header[:]); err != nil { return result, err @@ -81,18 +99,23 @@ func readTIFFLayout(input io.Reader) (tiffImageLayout, error) { offset := int64(order.Uint32(header[4:])) position := int64(8) var seen map[uint32]bool + var transparency *tiffImageLayout for offset != 0 { if seen != nil && seen[uint32(offset)] { return result, fmt.Errorf("cyclic TIFF directory chain") } - layout, end, err := readTIFFDirectory(input, order, offset, position) + layout, end, err := readTIFFDirectory(ctx, input, order, offset, position) if err != nil { return result, err } if layout.mask { - return result, fmt.Errorf("TIFF transparency mask directories are not supported") - } - if !layout.reduced || layout.next == 0 && seen == nil { + if !layout.reduced { + if transparency != nil { + return result, fmt.Errorf("multiple TIFF transparency masks") + } + transparency = &layout + } + } else if !layout.reduced || layout.next == 0 && seen == nil { if result.offset != 0 { return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources") } @@ -106,17 +129,30 @@ func readTIFFLayout(input io.Reader) (tiffImageLayout, error) { } offset, position = int64(layout.next), end } + if err := ctx.Err(); err != nil { + return tiffImageLayout{}, err + } if result.offset == 0 { return result, fmt.Errorf("missing TIFF main image") } + result.transparency = transparency + if transparency != nil { + size := result.displaySize() + if size.X > int(^uint(0)>>1)/8/size.Y { + return tiffImageLayout{}, fmt.Errorf("TIFF mask image dimensions exceed capacity") + } + } return result, nil } // readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素 -// 入参: input 输入流, order 字节序, offset 目录偏移, position 当前流位置 -// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构或读取错误 -func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) { +// 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置 +// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误 +func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) { var result tiffImageLayout + if err := ctx.Err(); err != nil { + return result, position, err + } var header [4]byte if offset < 8 { return result, position, fmt.Errorf("invalid TIFF directory offset") @@ -129,7 +165,8 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position if offset < position { return result, position, errTIFFRandomAccess } - if _, err := io.CopyN(io.Discard, input, offset-position); err != nil { + reader := imageInput{ReadCloser: io.NopCloser(input), context: ctx} + if _, err := io.CopyN(io.Discard, reader, offset-position); err != nil { return result, position, err } } @@ -140,11 +177,16 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position result.orientation, result.offset, result.samples, result.fillOrder = 1, uint32(offset), 1, 1 result.depth = 1 result.compression = 1 + result.predictor = 1 var entry [12]byte var previous uint16 var subfile, legacy uint32 var hasSubfile bool + var invalidMaskDepth bool for i := range count { + if err := ctx.Err(); err != nil { + return result, position, err + } if _, err := io.ReadFull(input, entry[:]); err != nil { return result, position, err } @@ -153,20 +195,33 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position return result, position, fmt.Errorf("invalid TIFF directory order") } previous = tag - if tag == 258 && kind == 3 && number == 1 { - result.depth = uint32(order.Uint16(entry[8:10])) + if tag == 338 { + result.auxiliary = kind != 3 || number != 1 || order.Uint16(entry[8:]) == 0 } - if tag != 254 && tag != 255 && tag != 256 && tag != 257 && tag != 259 && tag != 262 && tag != 266 && tag != 274 && tag != 277 && tag != 284 && tag != 292 && tag != 293 { + if tag == 34675 { + if kind != 7 || number < 132 { + return result, position, fmt.Errorf("invalid TIFF color profile") + } + result.profileOffset, result.profileSize = order.Uint32(entry[8:]), number continue } - if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284) && kind != 3 || (tag == 254 || tag == 292 || tag == 293) && kind != 4 { + if tag == 258 { + invalidMaskDepth = kind != 3 || number != 1 + if !invalidMaskDepth { + result.depth = uint32(order.Uint16(entry[8:10])) + } + } + if tag != 254 && tag != 255 && tag != 256 && tag != 257 && tag != 259 && tag != 262 && tag != 266 && tag != 274 && tag != 277 && tag != 284 && tag != 292 && tag != 293 && tag != 317 && tag != 322 && tag != 323 { + continue + } + if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284 || tag == 317) && kind != 3 || (tag == 254 || tag == 292 || tag == 293) && kind != 4 { return result, position, fmt.Errorf("invalid TIFF image layout") } value := uint64(order.Uint32(entry[8:12])) if kind == 3 { value = uint64(order.Uint16(entry[8:10])) } - if (tag == 256 || tag == 257) && (value == 0 || value > uint64(int(^uint(0)>>1))) { + if (tag == 256 || tag == 257 || tag == 322 || tag == 323) && (value == 0 || value > uint64(int(^uint(0)>>1))) { return result, position, fmt.Errorf("invalid TIFF dimensions") } switch tag { @@ -181,6 +236,7 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position case 259: result.compression = uint32(value) case 262: + result.photometric = uint32(value) result.mask = value == 4 case 266: if value != 1 && value != 2 { @@ -206,6 +262,12 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position result.t4 = uint32(value) case 293: result.t6 = uint32(value) + case 317: + result.predictor = uint32(value) + case 322: + result.tileWidth = int(value) + case 323: + result.tileHeight = int(value) } } if _, err := io.ReadFull(input, header[:4]); err != nil { @@ -220,6 +282,12 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position if result.width == 0 || result.height == 0 { return result, position, fmt.Errorf("missing TIFF dimensions") } + if (result.tileWidth == 0) != (result.tileHeight == 0) || result.tileWidth%16 != 0 || result.tileHeight%16 != 0 { + return result, position, fmt.Errorf("invalid TIFF tile dimensions") + } + if result.mask && (invalidMaskDepth || result.photometric != 4 || result.samples != 1 || result.depth != 1) { + return result, position, fmt.Errorf("invalid TIFF transparency mask") + } return result, offset + 6 + int64(count)*12, nil } @@ -285,6 +353,17 @@ func (l tiffImageLayout) size() image.Point { return image.Pt(l.width, l.height) } +// displaySize 返回同时保留主图和透明蒙版分辨率的显示尺寸 +// 返回: image.Point 保留高分辨率蒙版边缘的像素宽高 +func (l tiffImageLayout) displaySize() image.Point { + size := l.size() + if l.transparency != nil { + mask := l.transparency.size() + size.X, size.Y = max(size.X, mask.X), max(size.Y, mask.Y) + } + return size +} + // Bounds 返回从零开始的显示区域 // 返回: image.Rectangle 显示区域 func (s *orientedTIFFImage) Bounds() image.Rectangle { diff --git a/ofdgo_image_tiff_fax.go b/ofdgo_image_tiff_fax.go index 2b76754..a817aa8 100644 --- a/ofdgo_image_tiff_fax.go +++ b/ofdgo_image_tiff_fax.go @@ -70,12 +70,12 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im } for _, tag := range []uint16{258, 317, 339} { v, err := value(tag, 1) - if err != nil || v != 1 { + if err != nil || v != 1 || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 { return nil, fmt.Errorf("invalid TIFF fax sample field %d", tag) } } photo, err := value(262, 2) - if err != nil || photo > 1 || entries[338] != nil { + if err != nil || photo > 1 && photo != 4 || entries[338] != nil { return nil, fmt.Errorf("invalid TIFF fax color model") } width := uint32(layout.width) @@ -150,7 +150,7 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im params := pdfgo.Dictionary{ "K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows), "EndOfBlock": pdfgo.Boolean(false), "EndOfLine": pdfgo.Boolean(layout.compression == 3), - "EncodedByteAlign": pdfgo.Boolean(layout.compression == 2), "BlackIs1": pdfgo.Boolean(photo == 1), + "EncodedByteAlign": pdfgo.Boolean(layout.compression == 2), "BlackIs1": pdfgo.Boolean(photo == 1 || photo == 4), } object := &pdfgo.Image{Width: int(width), Height: int(rows), BitsPerComponent: 1, ColorSpace: pdfgo.Name("DeviceGray"), Stream: &pdfgo.Stream{Dictionary: pdfgo.Dictionary{"Filter": pdfgo.Name("CCITTFaxDecode"), "DecodeParms": params}, Data: encoded}} pixels, err := object.DecodeSamplesContext(ctx) diff --git a/ofdgo_image_tiff_mask.go b/ofdgo_image_tiff_mask.go new file mode 100644 index 0000000..dca3aaf --- /dev/null +++ b/ofdgo_image_tiff_mask.go @@ -0,0 +1,85 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "context" + "image" + "image/color" +) + +// maskedTIFFImage 将独立透明蒙版叠加到主图,复用两个目录的只读像素 +type maskedTIFFImage struct { + source, mask image.Image + bounds image.Rectangle +} + +// decodeTIFFImage 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版 +// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 +// 返回: image.Image 图像, error 解码或取消错误 +func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) { + source, err := decodeTIFFPixels(ctx, data, layout) + if err != nil || layout.transparency == nil { + return source, err + } + mask, err := decodeTIFFPixels(ctx, data, *layout.transparency) + if err != nil { + return nil, err + } + return &maskedTIFFImage{source: source, mask: mask, bounds: image.Rectangle{Max: layout.displaySize()}}, nil +} + +// Bounds 返回主图和蒙版共同覆盖的像素区域 +// 返回: image.Rectangle 显示区域 +func (s *maskedTIFFImage) Bounds() image.Rectangle { return s.bounds } + +// ColorModel 返回保留原图采样精度的非预乘颜色模型 +// 返回: color.Model 颜色模型 +func (s *maskedTIFFImage) ColorModel() color.Model { return color.NRGBA64Model } + +// NRGBA64At 按像素中心读取主图及蒙版,零位隐藏原图并保留隐藏颜色 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.NRGBA64 非预乘十六位颜色 +func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 { + if !image.Pt(x, y).In(s.bounds) { + return color.NRGBA64{} + } + size := s.bounds.Size() + x, y = x-s.bounds.Min.X, y-s.bounds.Min.Y + b := s.source.Bounds() + sx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) + sy := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y)) + pixel := imageNRGBA64At(s.source, sx, sy) + b = s.mask.Bounds() + mx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X)) + my := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y)) + if imageNRGBA64At(s.mask, mx, my).R == 0 { + pixel.A = 0 + } + return pixel +} + +// At 返回非预乘颜色,区域外透明 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.Color 像素颜色 +func (s *maskedTIFFImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) } + +// RGBA64At 返回预乘十六位颜色,供采样器直接读取 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.RGBA64 预乘颜色 +func (s *maskedTIFFImage) RGBA64At(x, y int) color.RGBA64 { + r, g, b, a := s.NRGBA64At(x, y).RGBA() + return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} +} diff --git a/ofdgo_image_tiff_planar.go b/ofdgo_image_tiff_planar.go index 9b83063..cbb7f07 100644 --- a/ofdgo_image_tiff_planar.go +++ b/ofdgo_image_tiff_planar.go @@ -33,34 +33,73 @@ type tiffPlaneInput struct { ctx context.Context } -// planarTIFFImage 组合独立分量或映射调色板,不额外分配交错像素缓冲 +// planarTIFFImage 组合颜色与Alpha分量,保留交错步长并按需映射调色板 type planarTIFFImage struct { - planes []image.Image - bounds image.Rectangle - depth uint32 - packed [4]uint32 - alpha uint32 - gray bool - palette []color.NRGBA64 + planes []image.Image + bounds image.Rectangle + depth uint32 + packed [5]uint32 + offsets [5]int + alpha uint32 + gray bool + cmyk bool + dotRange [4][2]uint16 + palette []color.NRGBA64 + stride int + invert bool + predictorWidth int } -// decodeTIFFImage 按存储方式解码主图并统一显示方向 -// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 -// 返回: image.Image 图像, error 解码或取消错误 -func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) { - if layout.compression >= 2 && layout.compression <= 4 { - pixels, err := decodeTIFFFax(ctx, data, layout) - if err == nil && layout.orientation != 1 { - pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout} +// needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用 +// 返回: bool 是否需要分量视图 +func (layout tiffImageLayout) needsSampleView() bool { + return layout.mask || layout.auxiliary || layout.photometric == 5 || layout.planar && layout.samples > 1 || layout.samples == 2 || layout.depth < 8 && (layout.samples > 1 || layout.depth > 1 || layout.predictor == 2) +} + +// decodedPixelBytes 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量 +// 返回: uint64 每像素预算字节数 +func (layout tiffImageLayout) decodedPixelBytes() uint64 { + samples := uint64(layout.samples) + if layout.planar { + if layout.photometric == 5 && samples > 4 { + return 20 } - return pixels, err + return 16 } - ranges, err := tiffBitOrderRanges(ctx, data, layout) + if !layout.needsSampleView() { + return 8 + } + bytes := uint64(1) + if layout.depth == 2 || layout.depth == 4 { + bytes = uint64(layout.depth) + } else if layout.depth > 8 { + bytes = 2 + } + return max(uint64(8), samples*bytes) +} + +// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向 +// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局 +// 返回: image.Image 图像, error 解码或取消错误 +func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) { + profile, err := readTIFFColorProfile(ctx, data, layout) if err != nil { return nil, err } + var ranges [][2]int64 + if layout.compression < 2 || layout.compression > 4 { + ranges, err = tiffBitOrderRanges(ctx, data, layout) + if err != nil { + return nil, err + } + } var pixels image.Image - if layout.planar && layout.samples > 1 || layout.samples == 1 && (layout.depth == 2 || layout.depth == 4) { + if layout.compression >= 2 && layout.compression <= 4 { + pixels, err = decodeTIFFFax(ctx, data, layout) + if err != nil { + return nil, err + } + } else if layout.needsSampleView() { result, directories, err := tiffPlaneDirectories(ctx, data, layout) if err != nil { return nil, err @@ -74,6 +113,11 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) ( if err != nil { return nil, err } + if result.predictorWidth != 0 && (result.stride > 1 || result.packed[len(result.planes)] != 0) { + if err := restoreTIFFPrediction(ctx, plane, max(result.stride, 1), result.predictorWidth, result.packed[len(result.planes)]); err != nil { + return nil, err + } + } result.planes = append(result.planes, plane) } pixels = result @@ -86,6 +130,12 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) ( return nil, err } } + if profile != nil { + pixels, err = applyTIFFColorProfile(ctx, pixels, profile) + if err != nil { + return nil, err + } + } if ctx != nil { if err := ctx.Err(); err != nil { return nil, err @@ -97,7 +147,7 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) ( return pixels, nil } -// tiffPlaneDirectories 为颜色、透明度或调色板索引构建单通道TIFF目录 +// tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误 func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayout) (*planarTIFFImage, [][]byte, error) { @@ -131,11 +181,11 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo return tiffPlaneValue(data, order, entry, 0) } samples, err := value(277, 1) - if err != nil || samples < 1 || samples > 4 { + if err != nil || samples < 1 || samples > math.MaxUint16 { return nil, nil, fmt.Errorf("invalid TIFF plane count") } photo, err := value(262, math.MaxUint32) - if err != nil || photo > 3 || photo == 3 && samples != 1 { + if err != nil || photo > 5 { return nil, nil, fmt.Errorf("unsupported TIFF planar color model") } compression, err := value(259, 1) @@ -143,22 +193,55 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo return nil, nil, err } fill, err := value(266, 1) - if err != nil || fill != 1 && (fill != 2 || compression != 1 && compression != 3 && compression != 4) { + if err != nil || fill != 1 && (fill != 2 || compression != 1 && (compression < 2 || compression > 4)) { return nil, nil, fmt.Errorf("unsupported TIFF plane fill order") } predictor, err := value(317, 1) if err != nil || predictor != 1 && predictor != 2 { return nil, nil, fmt.Errorf("unsupported TIFF plane predictor") } - result := &planarTIFFImage{bounds: image.Rect(0, 0, layout.width, layout.height), gray: photo < 2} + result := &planarTIFFImage{bounds: image.Rect(0, 0, layout.width, layout.height), gray: photo < 2 || photo == 4, invert: photo == 0, cmyk: photo == 5} + if result.cmyk { + for _, tag := range []uint16{332, 334} { + want := uint32(1) + if tag == 334 { + want = 4 + } + got, err := value(tag, want) + if err != nil || got != want || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 { + return nil, nil, fmt.Errorf("unsupported TIFF ink configuration") + } + } + } + planes := samples + if !layout.planar && samples > 1 { + result.stride = int(samples) + planes = 1 + } colors := uint32(3) - if photo != 2 { + if result.cmyk { + colors = 4 + } else if photo != 2 { colors = 1 } if samples != colors { - result.alpha, err = value(338, 0) - if err != nil || samples != colors+1 || result.alpha != 1 && result.alpha != 2 { - return nil, nil, fmt.Errorf("unsupported TIFF extra planes") + entry := entries[338] + if samples < colors || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples-colors { + return nil, nil, fmt.Errorf("invalid TIFF extra samples") + } + for i := uint32(0); i < samples-colors; i++ { + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, nil, err + } + } + alpha, err := tiffPlaneValue(data, order, entry, i) + if err != nil || alpha > 2 || alpha != 0 && result.alpha != 0 { + return nil, nil, fmt.Errorf("unsupported TIFF extra samples") + } + if alpha != 0 { + result.alpha, result.offsets[colors] = alpha, int(i) + } } } else if entries[338] != nil { return nil, nil, fmt.Errorf("unexpected TIFF extra samples") @@ -183,16 +266,16 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo } blocks = (uint64(layout.height)-1)/uint64(rows) + 1 } - if blocks > math.MaxUint32/uint64(samples) { + if blocks > math.MaxUint32/uint64(planes) { return nil, nil, fmt.Errorf("too many TIFF plane blocks") } for _, tag := range []uint16{locations, lengths} { entry := entries[tag] - if entry == nil || uint64(order.Uint32(entry[4:])) != blocks*uint64(samples) || tag == 324 && order.Uint16(entry[2:]) != 4 { + if entry == nil || uint64(order.Uint32(entry[4:])) != blocks*uint64(planes) || tag == 324 && order.Uint16(entry[2:]) != 4 { return nil, nil, fmt.Errorf("invalid TIFF plane block count") } } - for i := uint32(0); i < uint32(blocks)*samples; i++ { + for i := uint32(0); i < uint32(blocks)*planes; i++ { if ctx != nil { if err := ctx.Err(); err != nil { return nil, nil, err @@ -213,7 +296,21 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo } } var directories [][]byte + var firstDepth uint32 for plane := uint32(0); plane < samples; plane++ { + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, nil, err + } + } + slot := plane + visible := plane < colors || result.alpha != 0 && plane == colors+uint32(result.offsets[colors]) + if !visible && result.stride == 0 { + continue + } + if plane >= colors { + slot = colors + } depth := uint32(1) if entry := entries[258]; entry != nil { depth, err = tiffPlaneValue(data, order, entry, plane) @@ -221,26 +318,80 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo if err != nil || depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 { return nil, nil, fmt.Errorf("unsupported TIFF plane depth") } + if plane == 0 { + firstDepth = depth + } else if result.stride > 1 && depth != firstDepth { + return nil, nil, fmt.Errorf("unsupported TIFF mixed interleaved sample depths") + } + if !visible { + continue + } result.depth = max(result.depth, depth) - if depth < 8 && predictor != 1 { - return nil, nil, fmt.Errorf("unsupported TIFF packed plane predictor") + if result.cmyk && plane < 4 { + low, high := uint32(0), uint32(1< 4 { + at := uint64(order.Uint32(values)) + if at+uint64(n) > uint64(len(data)) { + return nil, nil, fmt.Errorf("truncated TIFF dot range") + } + values = data[at : at+uint64(n)] + } + low, high = uint32(values[i]), uint32(values[i+1]) + } else { + low, err = tiffPlaneValue(data, order, entry, i) + if err != nil { + return nil, nil, err + } + high, err = tiffPlaneValue(data, order, entry, i+1) + } + if err != nil || low == high || low > 1< 1< uint64(math.MaxInt)/uint64(layout.height) || uint64(layout.width)*uint64(depth) > math.MaxUint32 || uint64(width)*uint64(depth) > math.MaxUint32 { - return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit") - } + result.packed[slot] = depth + factor *= uint64(depth) if compression == 2 || compression == 3 || compression == 4 { return nil, nil, fmt.Errorf("unsupported TIFF packed plane compression") } } + pixelBytes := factor + if depth > 8 { + pixelBytes *= 2 + } + if uint64(layout.width)*pixelBytes > uint64(math.MaxInt)/uint64(layout.height) || uint64(layout.width)*factor > math.MaxUint32 || uint64(width)*factor > math.MaxUint32 { + return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit") + } + if predictor == 2 && (result.stride > 1 || depth < 8) { + result.predictorWidth = layout.width + if width != 0 { + result.predictorWidth = int(width) + } + } if entry := entries[339]; entry != nil { format, err := tiffPlaneValue(data, order, entry, plane) if err != nil || format != 1 { return nil, nil, fmt.Errorf("unsupported TIFF sample format") } } - if photo == 3 { + if photo == 3 && plane == 0 { entry := entries[320] if depth > 8 || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != 3< 1 && plane != 0 { + continue + } directory := make([]byte, 2, 6+count*12) for i := range count { if ctx != nil { @@ -271,33 +425,34 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo } entry := bytes.Clone(data[offset+2+i*12 : offset+14+i*12]) tag := order.Uint16(entry) - if tag == 338 || tag == 320 { + if tag == 338 || tag == 320 || tag >= 332 && tag <= 337 || tag == 34675 { continue } scalar := uint32(0) switch tag { case 258: scalar = depth - if result.packed[plane] != 0 { + if result.packed[slot] != 0 { scalar = 1 } case 256, 322: - if result.packed[plane] != 0 { + if factor != 1 { original, err := tiffPlaneValue(data, order, entry, 0) if err != nil { return nil, nil, err } order.PutUint16(entry[2:], 4) order.PutUint32(entry[4:], 1) - order.PutUint32(entry[8:], original*depth) + order.PutUint32(entry[8:], original*uint32(factor)) } case 262: scalar = 1 - if result.gray && plane == 0 && photo == 0 { - scalar = 0 - } case 277, 284, 339: scalar = 1 + case 317: + if predictor == 2 && (result.stride > 1 || depth < 8) { + order.PutUint16(entry[8:], 1) + } case locations, lengths: entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks)) if err != nil { @@ -318,6 +473,64 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo return result, directories, nil } +// restoreTIFFPrediction 在解码缓冲内恢复水平差分,各行及瓦片独立计算 +// 入参: ctx 取消上下文,可为nil, pixels 单通道解码视图, stride 每像素分量数, width 原始条带或瓦片宽度, packed 展开为一位采样的原位深,零表示未展开 +// 返回: error 图像类型或取消错误 +func restoreTIFFPrediction(ctx context.Context, pixels image.Image, stride, width int, packed uint32) error { + if ctx == nil { + ctx = context.Background() + } + var data []byte + var pitch, step int + switch pixels := pixels.(type) { + case *image.Gray: + data, pitch, step = pixels.Pix, pixels.Stride, max(int(packed), 1) + case *image.Gray16: + data, pitch, step = pixels.Pix, pixels.Stride, 2 + default: + return fmt.Errorf("unsupported TIFF predictor image") + } + bounds := pixels.Bounds() + columns := bounds.Dx() / max(int(packed), 1) + block := width * stride + for y := range bounds.Dy() { + if err := ctx.Err(); err != nil { + return err + } + row := data[y*pitch : y*pitch+columns*step] + for start := 0; start < columns; { + end := start + min(block, columns-start) + for x := start + stride; x < end; { + if err := ctx.Err(); err != nil { + return err + } + limit := x + min(4096, end-x) + for ; x < limit; x++ { + at, previous := x*step, (x-stride)*step + if packed != 0 { + var current, prior uint16 + for bit := range step { + current = current<<1 | uint16(row[at+bit]>>7) + prior = prior<<1 | uint16(row[previous+bit]>>7) + } + value := current + prior + for bit := range step { + row[at+bit] = byte(value>>uint(step-1-bit)&1) * 255 + } + } else if step == 2 { + value := binary.BigEndian.Uint16(row[at:]) + binary.BigEndian.Uint16(row[previous:]) + binary.BigEndian.PutUint16(row[at:], value) + } else { + row[at] += row[previous] + } + } + } + start = end + } + } + return ctx.Err() +} + // tiffPlaneValue 读取SHORT或LONG数组中的一项,校验完整数组范围 // 入参: data 原编码, order 字节序, entry 目录项, index 元素序号 // 返回: uint32 元素值, error 类型或越界错误 @@ -452,37 +665,94 @@ func (p *planarTIFFImage) components(x, y int) [4]uint16 { if !image.Pt(x, y).In(p.bounds) { return [4]uint16{} } - values := [4]uint16{0, 0, 0, 65535} - for i, plane := range p.planes { - var value uint16 - switch plane := plane.(type) { - case *image.Gray: - if bits := p.packed[i]; bits != 0 { - for bit := range int(bits) { - value = value<<1 | uint16(plane.GrayAt(x*int(bits)+bit, y).Y>>7) - } - value *= 65535 / uint16(1< 1 { + sx = x*p.stride + i + p.offsets[i] + } else { + plane = p.planes[i] + } + switch plane := plane.(type) { + case *image.Gray: + if bits := p.packed[i]; bits != 0 { + var value uint16 + for bit := range int(bits) { + value = value<<1 | uint16(plane.GrayAt(sx*int(bits)+bit, y).Y>>7) + } + return value * (65535 / uint16(1< uint64(len(data)) { + return nil, fmt.Errorf("truncated TIFF color profile") + } + kind := "" + switch layout.photometric { + case 0, 1: + kind = "GRAY" + case 2, 3: + kind = "RGB" + case 5: + kind = "CMYK" + default: + return nil, fmt.Errorf("unsupported TIFF profile color model") + } + profile, err := parseColorProfile(kind, data[at:at+size]) + if cancelErr := ctx.Err(); cancelErr != nil { + return nil, cancelErr + } + return profile, err +} + +// applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色 +// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置 +// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误 +func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *colorProfile) (image.Image, error) { + if ctx == nil { + ctx = context.Background() + } + if err := ctx.Err(); err != nil { + return nil, err + } + bounds := source.Bounds() + if bounds.Dx() <= 0 || bounds.Dy() <= 0 || bounds.Dx() > math.MaxInt/8/bounds.Dy() { + return nil, fmt.Errorf("TIFF profile image dimensions exceed capacity") + } + cmyk, _ := source.(*planarTIFFImage) + if cmyk != nil && !cmyk.cmyk { + cmyk = nil + } + model := source.ColorModel() + premultiplied := model == color.RGBAModel || model == color.RGBA64Model + table := newTIFFProfileTable(source) + result := image.NewNRGBA64(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + } + slot := -1 + if len(table) != 0 { + index, alpha := tiffProfileSample(source, x, y, len(table)) + slot = index + if pixel := table[index]; pixel.A != 0 { + pixel.A = alpha + result.SetNRGBA64(x, y, pixel) + continue + } + } + var values [4]float64 + var alpha uint16 + if cmyk != nil { + ink := cmyk.inksAt(x, y) + alpha = ink[4] + if cmyk.alpha == 1 && alpha == 0 { + continue + } + maximum := float64(65535) + if cmyk.alpha == 1 { + maximum = float64(alpha) + } + for i := range values { + values[i] = min(1, float64(ink[i])/maximum) + } + } else if premultiplied { + var pixel color.RGBA64 + if native, ok := source.(image.RGBA64Image); ok { + pixel = native.RGBA64At(x, y) + } else { + pixel = color.RGBA64Model.Convert(source.At(x, y)).(color.RGBA64) + } + alpha = pixel.A + if alpha == 0 { + continue + } + for i, value := range [3]uint16{pixel.R, pixel.G, pixel.B} { + values[i] = min(1, float64(value)/float64(alpha)) + } + } else { + pixel := imageNRGBA64At(source, x, y) + alpha = pixel.A + values = [4]float64{float64(pixel.R) / 65535, float64(pixel.G) / 65535, float64(pixel.B) / 65535} + } + rgb, err := profile.rgb(values) + if err != nil { + return nil, err + } + var channels [3]uint16 + for i, value := range rgb { + channels[i] = uint16(math.Round(max(0, min(1, value)) * 65535)) + } + if slot >= 0 { + table[slot] = color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: 65535} + } + result.SetNRGBA64(x, y, color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: alpha}) + } + } + if err := ctx.Err(); err != nil { + return nil, err + } + return result, nil +} + +// newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换 +// 入参: source 已解码且尺寸有效的图像 +// 返回: []color.NRGBA64 颜色表,Alpha非零表示已有结果,不适用时为nil +func newTIFFProfileTable(source image.Image) []color.NRGBA64 { + count := 0 + switch source := source.(type) { + case *image.Gray: + count = 256 + case *image.Gray16: + count = 65536 + case *image.Paletted: + count = len(source.Palette) + case *planarTIFFImage: + if source.alpha != 1 { + if len(source.palette) != 0 { + count = len(source.palette) + } else if source.gray { + count = 256 + if source.packed[0] != 0 { + count = 1 << source.packed[0] + } else if _, ok := source.planes[0].(*image.Gray16); ok { + count = 65536 + } + } + } + } + if count == 0 || count >= source.Bounds().Dx()*source.Bounds().Dy() { + return nil + } + return make([]color.NRGBA64, count) +} + +// tiffProfileSample 读取查找表索引与独立透明度,颜色键不包含透明度 +// 入参: source 适用查找表的图像, x 横坐标, y 纵坐标, count 颜色表长度 +// 返回: int 颜色索引, uint16 透明度 +func tiffProfileSample(source image.Image, x, y, count int) (int, uint16) { + switch source := source.(type) { + case *image.Gray: + return int(source.GrayAt(x, y).Y), 65535 + case *image.Gray16: + return int(source.Gray16At(x, y).Y), 65535 + case *image.Paletted: + index := source.ColorIndexAt(x, y) + _, _, _, alpha := source.Palette[index].RGBA() + return int(index), uint16(alpha) + case *planarTIFFImage: + index := int(uint32(source.sampleAt(0, x, y)) * uint32(count-1) / 65535) + alpha := uint16(65535) + if source.alpha != 0 { + alpha = source.sampleAt(1, x, y) + } + return index, alpha + } + return 0, 0 +} diff --git a/ofdgo_reader.go b/ofdgo_reader.go index ac9ad6b..096157a 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -548,17 +548,28 @@ func (r *Reader) AttachmentData(id string) ([]byte, error) { // 返回的文件路径可直接传入ResData读取 // 返回: []CustomTag 自定义标引列表, error 错误信息 func (r *Reader) CustomTags() ([]CustomTag, error) { - doc, err := r.Doc() + return r.CustomTagsContext(context.Background()) +} + +// CustomTagsContext 读取当前文档及版本的标引清单,取消或失败时不缓存未完成的清单 +// 不加载标引内容、页面、注解或签名;返回的文件路径可直接传入ResData读取 +// 入参: ctx 取消上下文 +// 返回: []CustomTag 自定义标引列表, error 读取、解析或取消错误 +func (r *Reader) CustomTagsContext(ctx context.Context) ([]CustomTag, error) { + doc, err := r.docStructureContext(ctx) if err != nil { return nil, err } if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil { var customTags CustomTags - partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags) + partPath, err := r.readDocumentPartContext(ctx, doc.CustomTags.Path, &customTags) if err != nil { return nil, err } for i := range customTags.CustomTag { + if err := ctx.Err(); err != nil { + return nil, err + } tag := &customTags.CustomTag[i] tag.SchemaLoc = r.resourceLocation(partPath, "", tag.SchemaLoc) tag.FileLoc = r.resourceLocation(partPath, "", tag.FileLoc) @@ -566,6 +577,9 @@ func (r *Reader) CustomTags() ([]CustomTag, error) { if customTags.CustomTag == nil { customTags.CustomTag = []CustomTag{} } + if err := ctx.Err(); err != nil { + return nil, err + } doc.CustomTags.CustomTag = customTags.CustomTag } return doc.CustomTags.CustomTag, nil @@ -1124,13 +1138,24 @@ func (r *Reader) resolvePageAreaContext(ctx context.Context, area PageArea, temp // 入参: loc 文件位置, value 文档结构 // 返回: string 文件路径, error 错误信息 func (r *Reader) readDocumentPart(loc string, value any) (string, error) { + return r.readDocumentPartContext(context.Background(), loc, value) +} + +// readDocumentPartContext 读取并解析文档关联文件,传递取消信号到文件读取和XML解码 +// 入参: ctx 取消上下文, loc 文件位置, value 文档结构 +// 返回: string 文件路径, error 读取、解析或取消错误 +func (r *Reader) readDocumentPartContext(ctx context.Context, loc string, value any) (string, error) { fullPath := r.ResPath(strings.TrimSpace(loc)) - data, err := r.readFile(fullPath) + data, err := r.readFileViewContext(ctx, fullPath) if err != nil { return "", err } - if err := xml.Unmarshal(data, value); err != nil { + decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) + if err := decoder.Decode(value); err != nil { return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err) } + if err := ctx.Err(); err != nil { + return "", err + } return fullPath, nil } diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index a3a5180..273a879 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -273,6 +273,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + } pixel := imageNRGBA64At(img, x, y) alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) opaque = opaque && alpha == 65535 @@ -295,14 +301,39 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva mask, ok := result.(*image.RGBA) if !ok { mask = image.NewRGBA(image.Rect(0, 0, w, h)) - draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src) + for y := 0; y < h; y++ { + for x := 0; x < w; { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + end := x + min(256, w-x) + draw.Draw(mask, image.Rect(x, y, end, y+1), result, result.Bounds().Min.Add(image.Pt(x, y)), draw.Src) + x = end + } + } } if mask.Rect.Min != (image.Point{}) { normalized := *mask normalized.Rect = mask.Rect.Sub(mask.Rect.Min) mask = &normalized } - opaque := mask.Opaque() + opaque := true + for y := 0; y < h && opaque; y++ { + row := mask.Pix[y*mask.Stride : y*mask.Stride+w*4] + for x := 3; x < len(row); x += 4 { + if (x-3)&1023 == 0 { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + } + if row[x] != 255 { + opaque = false + break + } + } + } if err := work.Err(); err != nil { r.renderError = err return img @@ -325,6 +356,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva row := out.Pix[y*out.Stride : y*out.Stride+w*4] pixels := src.Pix[offset : offset+w*4] for x := 0; x < len(row); x += 4 { + if x&1023 == 0 { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + } copy(row[x:x+3], pixels[x:x+3]) row[x+3] = uint8(int(pixels[x+3]) * int(row[x+3]) / 255) } @@ -341,6 +378,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } for x := bounds.Min.X; x < bounds.Max.X; x++ { + if (x-bounds.Min.X)&255 == 0 { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + } c := imageNRGBAAt(source, x, y) a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A c.A = uint8(int(c.A) * int(a) / 255) diff --git a/ofdgo_structure.go b/ofdgo_structure.go index 4036d5b..84d641b 100644 --- a/ofdgo_structure.go +++ b/ofdgo_structure.go @@ -41,6 +41,7 @@ type Structure struct { } // StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML +// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本 // References仅解释公文标引中的ObjectRef;未知扩展保留结构,不推定其引用语义 type StructureNode struct { Name string `json:"name"` @@ -88,38 +89,48 @@ func (r *Reader) StructureContext(ctx context.Context) ([]Structure, error) { if err := ctx.Err(); err != nil { return nil, err } - tags, err := r.CustomTags() + tags, err := r.CustomTagsContext(ctx) if err != nil { return nil, err } result := make([]Structure, len(tags)) for i, tag := range tags { - item := &result[i] - *item = Structure{TypeID: tag.TypeID, Namespace: tag.NameSpace, FileLoc: tag.FileLoc} - data, err := r.readFileViewContext(ctx, r.ResPath(tag.FileLoc)) - if err == nil { - item.Root, err = parseStructure(ctx, data, tag) - if err == nil { - item.CanBind = structureOfficial(tag, item.Root.Namespace) - } - } + item, err := r.readStructureContext(ctx, tag) if ctx.Err() != nil { return nil, ctx.Err() } if err != nil { item.Error = err.Error() } + result[i] = item } return result, nil } +// readStructureContext 读取单个标引文件,解析失败时保留入口信息及原始错误 +// 入参: ctx 取消上下文, tag 标引入口 +// 返回: Structure 标引结构, error 文件读取、解析或取消错误 +func (r *Reader) readStructureContext(ctx context.Context, tag CustomTag) (Structure, error) { + item := Structure{TypeID: tag.TypeID, Namespace: tag.NameSpace, FileLoc: tag.FileLoc} + data, err := r.readFileViewContext(ctx, r.ResPath(tag.FileLoc)) + if err == nil { + item.Root, err = parseStructure(ctx, data, tag) + } + if err != nil { + return item, fmt.Errorf("read custom tag %s: %w", tag.FileLoc, err) + } + item.CanBind = structureOfficial(tag, item.Root.Namespace) + return item, nil +} + // parseStructure 解析有界XML结构,按GB/T33476.3的ObjectRef规则识别公文引用 // 入参: ctx 取消上下文, data 标引XML, tag 标引入口 // 返回: *StructureNode 根节点, error 解析、限额或取消错误 func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*StructureNode, error) { - d := xml.NewDecoder(bytes.NewReader(data)) + d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) var root *StructureNode var stack []*StructureNode + var text []*strings.Builder count := 0 for { if err := ctx.Err(); err != nil { @@ -138,6 +149,9 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure if len(stack) >= 128 || count > 100000 { return nil, fmt.Errorf("custom tag structure limit exceeded") } + if len(stack) == len(text) { + text = append(text, new(strings.Builder)) + } node := StructureNode{Name: token.Name.Local, Namespace: token.Name.Space} for _, attr := range token.Attr { if attr.Name.Space != "xmlns" && !(attr.Name.Space == "" && attr.Name.Local == "xmlns") { @@ -157,13 +171,15 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure } case xml.CharData: if len(stack) > 0 { - stack[len(stack)-1].Text += string(token) - } else if strings.TrimSpace(string(token)) != "" { + text[len(stack)-1].Write(token) + } else if strings.Trim(string(token), " \t\r\n") != "" { return nil, fmt.Errorf("text outside custom tag root") } case xml.EndElement: node := stack[len(stack)-1] - node.Text = strings.TrimSpace(node.Text) + builder := text[len(stack)-1] + node.Text = builder.String() + builder.Reset() official := structureOfficial(tag, root.Namespace) for _, parent := range stack { official = official && parent.Namespace == root.Namespace @@ -227,45 +243,71 @@ func (r *Renderer) StructureTargets(index int, path []int) ([]StructureTarget, e return r.StructureTargetsContext(context.Background(), index, path) } -// StructureTargetsContext 在当前文档和版本内解析标引目标,不搜索其他文档的同名图元 +// StructureTargetsContext 仅读取所选标引,在当前文档和版本内定位目标,不搜索其他文档的同名图元 // 入参: ctx 取消上下文, index 标引清单索引, path 子节点索引路径 // 返回: []StructureTarget 有序目标, error 解析、渲染或取消错误 func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path []int) ([]StructureTarget, error) { - structures, err := r.Reader.StructureContext(ctx) + tags, err := r.Reader.CustomTagsContext(ctx) if err != nil { return nil, err } - if index < 0 || index >= len(structures) || structures[index].Root == nil { + if index < 0 || index >= len(tags) { return nil, fmt.Errorf("custom tag structure not found: %d", index) } - node := structures[index].Root + structure, err := r.Reader.readStructureContext(ctx, tags[index]) + if err != nil { + return nil, err + } + node := structure.Root for _, i := range path { + if err := ctx.Err(); err != nil { + return nil, err + } if i < 0 || i >= len(node.Children) { return nil, fmt.Errorf("custom tag node not found") } node = &node.Children[i] } + var references []StructureReference + var collect func(*StructureNode) error + collect = func(node *StructureNode) error { + if err := ctx.Err(); err != nil { + return err + } + references = append(references, node.References...) + for i := range node.Children { + if err := collect(&node.Children[i]); err != nil { + return err + } + } + return nil + } + if err := collect(node); err != nil { + return nil, err + } + if len(references) == 0 { + return nil, ctx.Err() + } doc, err := r.Reader.DocContext(ctx) if err != nil { return nil, err } pages := signaturePageNumbers(doc) var result []StructureTarget - var collect func(*StructureNode) - collect = func(node *StructureNode) { - for _, ref := range node.References { - for _, id := range ref.ObjectIDs { - page, _ := resourceValue(pages, ref.PageID) - result = append(result, StructureTarget{Page: page, PageID: ref.PageID, ID: id}) + for _, ref := range references { + page, _ := resourceValue(pages, ref.PageID) + for _, id := range ref.ObjectIDs { + if err := ctx.Err(); err != nil { + return nil, err } - } - for i := range node.Children { - collect(&node.Children[i]) + result = append(result, StructureTarget{Page: page, PageID: ref.PageID, ID: id}) } } - collect(node) byPage := make(map[int]map[string]*StructureTarget) for i := range result { + if err := ctx.Err(); err != nil { + return nil, err + } target := &result[i] if target.Page == 0 { target.Error = "custom tag page not found" @@ -277,6 +319,8 @@ func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path byPage[target.Page][editorResourceID(target.ID)] = target target.Error = "custom tag object not found" } + view := *r + view.RenderAnnotations, view.pageContext = true, ctx for page, targets := range byPage { if err := ctx.Err(); err != nil { return nil, err @@ -285,12 +329,15 @@ func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path if err != nil { return nil, err } - visitor := &structureVisitor{ctx: ctx, renderer: r, targets: targets} - if err := r.WalkPage(content, visitor); err != nil { + visitor := &structureVisitor{ctx: ctx, renderer: &view, targets: targets} + if err := view.WalkPage(content, visitor); err != nil { return nil, err } } for i, target := range result { + if err := ctx.Err(); err != nil { + return nil, err + } if source := byPage[target.Page][editorResourceID(target.ID)]; source != nil { result[i] = *source } diff --git a/ofdgo_structure_object.go b/ofdgo_structure_object.go index 1416c4d..4cc6fb3 100644 --- a/ofdgo_structure_object.go +++ b/ofdgo_structure_object.go @@ -14,28 +14,47 @@ package ofdgo -// structureObjectVisitor 收集页面及复合图元标识,绑定校验不解码字体、图片或裁剪 +import "context" + +// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪 type structureObjectVisitor struct { - objects map[string]bool - targets []StructureTarget + objects map[string]bool + targets []StructureTarget + listTargets bool } -// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容和复合图元成员 +// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观 // 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次 // 入参: page 从0开始的页面索引 // 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误 func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) { - content, err := r.Reader.PageContentByIndex(page) + return r.StructureObjectsContext(context.Background(), page) +} + +// StructureObjectsContext 列出页面绑定候选,读取或遍历取消时不返回部分结果,不改变渲染选项 +// 入参: ctx 取消上下文, page 从0开始的页面索引 +// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面读取、遍历或取消错误 +func (r *Renderer) StructureObjectsContext(ctx context.Context, page int) ([]StructureTarget, error) { + content, err := r.Reader.PageContentByIndexContext(ctx, page) if err != nil { return nil, err } - visitor := &structureObjectVisitor{objects: make(map[string]bool)} - if err := r.WalkPage(content, visitor); err != nil { + visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true} + view := *r + view.RenderAnnotations = true + view.pageContext = ctx + if err := view.WalkPage(content, visitor); err != nil { return nil, err } for i := range visitor.targets { + if err := ctx.Err(); err != nil { + return nil, err + } visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID } + if err := ctx.Err(); err != nil { + return nil, err + } return visitor.targets, nil } @@ -46,6 +65,9 @@ func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool { id := editorResourceID(editorObjectID(*object)) if id != "" && !v.objects[id] { v.objects[id] = true + if !v.listTargets { + return false + } target := StructureTarget{ID: id} if object.Type == "TextObject" { target.Text = object.TextObject.Text() @@ -58,7 +80,7 @@ func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool { // EndObject 满足分组访问接口,不维护分组栈 func (v *structureObjectVisitor) EndObject() {} -// BeginAnnotation 不为注解容器分配正文图元标识 +// BeginAnnotation 不将注释容器编号作为图元标识 // 入参: id 注解标识 // 返回: bool 始终为false func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false } diff --git a/ofdgo_structure_xml.go b/ofdgo_structure_xml.go index 7f6de43..a5900c9 100644 --- a/ofdgo_structure_xml.go +++ b/ofdgo_structure_xml.go @@ -42,8 +42,11 @@ func encodeStructureNode(node StructureNode) ([]byte, error) { return err } start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}} - if node.Namespace == "" { + switch node.Namespace { + case "": start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""}) + case "http://www.w3.org/XML/1998/namespace": + start.Name = xml.Name{Local: "xml:" + node.Name} } for _, attr := range node.Attributes { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value}) diff --git a/ofdgo_version_edit.go b/ofdgo_version_edit.go index d693327..25dba5d 100644 --- a/ofdgo_version_edit.go +++ b/ofdgo_version_edit.go @@ -144,6 +144,28 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s // 入参: ctx 取消上下文 // 返回: map[string]bool 受保护路径, error 读取或结构错误 func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) { + shared, err := r.versionPreservedFiles(ctx) + if err != nil { + return nil, err + } + var indexes []int + for index := range r.DocumentCount() { + if index != r.documentIndex { + indexes = append(indexes, index) + } + } + files, err := r.documentFiles(ctx, indexes...) + if err != nil { + return nil, err + } + maps.Copy(shared, files) + return shared, nil +} + +// versionPreservedFiles 汇总当前文档封面、未选版本及原始入口依赖,不包含其他文档 +// 入参: ctx 取消上下文 +// 返回: map[string]bool 需要保留的文件, error 读取或取消错误 +func (r *Reader) versionPreservedFiles(ctx context.Context) (map[string]bool, error) { shared := make(map[string]bool) data, err := r.readFileViewContext(ctx, "OFD.xml") if err != nil { @@ -163,17 +185,6 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error shared[name] = true } } - var indexes []int - for index := range r.DocumentCount() { - if index != r.documentIndex { - indexes = append(indexes, index) - } - } - files, err := r.documentFiles(ctx, indexes...) - if err != nil { - return nil, err - } - maps.Copy(shared, files) versions, err := r.DocumentVersionsContext(ctx) if err != nil { return nil, err @@ -204,21 +215,22 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error return shared, nil } -// versionResourceFiles 限定当前版本的资源扫描范围,合并其他文档依赖以保护跨文档引用 +// versionResourceFiles 分别记录当前入口依赖与完整扫描范围,其他文档和版本清单用于保护共享引用 // 入参: ctx 取消上下文, parts 待写入条目, generated 本次自产页面及引用 -// 返回: map[string]bool 扫描路径,非版本文档返回nil, error 读取或结构错误 -func (r *Reader) versionResourceFiles(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) (map[string]bool, error) { +// 返回: map[string]bool 当前入口依赖, map[string]bool 扫描范围,非版本文档均为nil, error 读取或结构错误 +func (r *Reader) versionResourceFiles(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) (map[string]bool, map[string]bool, error) { if r == nil || !r.OFD.DocBody[r.documentIndex].versioned { - return nil, nil + return nil, nil, nil } entry, err := r.DocumentRootContext(ctx) if err != nil { - return nil, err + return nil, nil, err } files, err := r.versionFileDependencies(ctx, entry, parts, generated) if err != nil { - return nil, err + return nil, nil, err } + selected := maps.Clone(files) if r.versionInfo != nil { for _, file := range r.versionInfo.Files { files[file.Location] = true @@ -232,10 +244,10 @@ func (r *Reader) versionResourceFiles(ctx context.Context, parts map[string][]by } other, err := r.documentFiles(ctx, indexes...) if err != nil { - return nil, err + return nil, nil, err } maps.Copy(files, other) - return files, nil + return selected, files, nil } // versionChanges 隔离当前文档或版本修改涉及的共享文件,同步版本清单,不修改输入包或修改集