feat(电子公文): 完善标引注释,优化资源处理
build.yaml / build (push) Successful in 48s

This commit is contained in:
xiaoqidun committed 2026-10-13 08:30:03 +08:00
1 parent 3f797e656b
commit 91e5b57e24
43 files changed
+1975 -341

No files matched your search

+6 -2
View File
@@ -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()) {
+4 -1
View File
@@ -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()
}
+15 -2
View File
@@ -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]
}
+38 -4
View File
@@ -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 取消错误
+29 -7
View File
@@ -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]
+11 -6
View File
@@ -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
+5 -10
View File
@@ -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)
}
+10 -5
View File
@@ -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)
}
+8 -4
View File
@@ -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
+5 -4
View File
@@ -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
+9
View File
@@ -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.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.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
}
+6 -4
View File
@@ -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 将默认点击范围固定到对象坐标,保留动作原文及显式区域
+20 -5
View File
@@ -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)
})
}
+1
View File
@@ -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
}
+11
View File
@@ -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)
+8
View File
@@ -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 裁剪或取消错误
+3
View File
@@ -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
+88 -11
View File
@@ -17,6 +17,7 @@ package ofdgo
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"maps"
@@ -83,25 +84,16 @@ 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":
if candidate {
candidates[name] = true
}
}
}
}
}
if err := pruneExternalFontFiles(ctx, view, slices.Collect(maps.Keys(candidates))...); err != nil {
return err
}
@@ -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 读取、裁剪或取消错误
+3 -1
View File
@@ -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,9 +176,11 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
origins[id] = &copy
result = append(result, id)
}
return e.remapStructureObjects(page, remaps, func() error {
e.replaceGroup(page, indexes, objects, origins)
return nil
})
})
return result, err
}
+30 -6
View File
@@ -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 {
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+31 -7
View File
@@ -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)
}
+9 -4
View File
@@ -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) {
+15 -2
View File
@@ -26,11 +26,12 @@ import (
// editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容
const editorVectorUsageLimit = 4096
// editorGeneratedReferences 保存本次输出条目及其引用,不跨写出复用
// editorGeneratedReferences 保存本次输出条目、资源引用及完整图元编号,不跨写出复用
type editorGeneratedReferences struct {
data []byte
refs *editorResourceRefs
staged *editorStagedPage
objects map[string]bool
}
// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效
@@ -40,10 +41,11 @@ type editorVectorUsage struct {
refs *editorResourceRefs
}
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描
// editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描
type editorReferenceScan struct {
refs *editorResourceRefs
objects map[string]bool
graphicIDs map[string]bool
name string
base string
stack []string
@@ -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 {
+6 -6
View File
@@ -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 ""
}
+373 -38
View File
@@ -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
}
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)
snapshotPages = make(map[string]Page, len(doc.Pages.Page))
for _, page := range doc.Pages.Page {
snapshotPages[editorResourceID(page.ID)] = page
}
}
scan(root)
for id := range source.nodes {
id = editorResourceID(id)
if id != "" && !remaining[id] {
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
}
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)
}
}
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, ok = parts[name]
if !ok {
data, err = e.source.reader.readFileViewContext(ctx, name)
if err != nil {
return err
}
}
updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string) {
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
}
}
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
+75 -25
View File
@@ -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,17 +253,21 @@ 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)
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 {
return "", nil, nil, nil, err
@@ -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 r == nil {
var err error
r, err = e.Reader()
if err != nil {
return err
return nil, err
}
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
}
+104 -19
View File
@@ -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
}
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))
var cfg image.Config
if layout.needsSampleView() {
img, _, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.size()
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
}
}
if profile != nil || layout.transparency != nil {
cfg.ColorModel = color.NRGBA64Model
}
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++ {
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
}
for x := bounds.Min.X; x < bounds.Max.X; x++ {
}
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++ {
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
}
for x := bounds.Min.X; x < bounds.Max.X; x++ {
}
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 {
+1 -1
View File
@@ -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" {
+21 -6
View File
@@ -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中的方向和密度,不跟随缩略图与其他目录
+95 -19
View File
@@ -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
return &resampledImageMask{source: mask, original: mask.Bounds(), bounds: image.Rectangle{Max: size}}, ctx.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
// 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{}
}
result.SetGray(x, y, color.Gray{Y: value})
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)
}
}
return result, ctx.Err()
// 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)
}
+93 -14
View File
@@ -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 {
if transparency != nil {
return result, fmt.Errorf("multiple TIFF transparency masks")
}
if !layout.reduced || layout.next == 0 && seen == nil {
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 {
+3 -3
View File
@@ -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)
+85
View File
@@ -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)}
}
+330 -60
View File
@@ -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
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 主图布局
// 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 16
}
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 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}
}
return pixels, err
}
ranges, err := tiffBitOrderRanges(ctx, data, layout)
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<<depth-1)
if entry := entries[336]; entry != nil {
n := order.Uint32(entry[4:])
if kind := order.Uint16(entry[2:]); kind != 1 && kind != 3 || n != 2 && n != 2*samples {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
i := plane * 2
if n == 2 {
i = 0
}
if order.Uint16(entry[2:]) == 1 {
values := entry[8:]
if n > 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<<depth-1 || high > 1<<depth-1 {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
}
result.dotRange[plane] = [2]uint16{uint16(low * 65535 / (1<<depth - 1)), uint16(high * 65535 / (1<<depth - 1))}
}
factor := uint64(max(result.stride, 1))
if depth == 1 && predictor == 2 {
result.packed[slot] = 1
}
if depth == 2 || depth == 4 {
result.packed[plane] = depth
if uint64(layout.width)*uint64(depth) > 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<<depth {
return nil, nil, fmt.Errorf("invalid TIFF color map")
@@ -262,6 +413,9 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
result.palette[i] = color.NRGBA64{R: components[0], G: components[1], B: components[2], A: 65535}
}
}
if result.stride > 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{}
}
if p.cmyk {
ink := p.inksAt(x, y)
maximum := uint32(65535)
if p.alpha == 1 {
maximum = uint32(ink[4])
if maximum == 0 {
return [4]uint16{}
}
}
black := maximum - min(uint32(ink[3]), maximum)
var values [4]uint16
for i := range 3 {
values[i] = uint16((maximum - min(uint32(ink[i]), maximum)) * black / maximum)
}
values[3] = ink[4]
return values
}
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)
count := 3
if p.gray || len(p.palette) != 0 {
count = 1
}
value *= 65535 / uint16(1<<bits-1)
} else {
value = uint16(plane.GrayAt(x, y).Y) * 257
if p.alpha != 0 {
count++
}
case *image.Gray16:
value = plane.Gray16At(x, y).Y
default:
v, _, _, _ := plane.At(x, y).RGBA()
value = uint16(v)
for i := range count {
value := p.sampleAt(i, x, y)
if i == 0 && p.invert {
value = 65535 - value
}
if len(p.palette) != 0 {
if len(p.palette) != 0 && i == 0 {
c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535]
return [4]uint16{c.R, c.G, c.B, c.A}
}
if p.gray && i == 0 {
values = [4]uint16{c.R, c.G, c.B, c.A}
} else if p.gray && i == 0 {
values[0], values[1], values[2] = value, value, value
} else if p.gray {
} else if p.gray || len(p.palette) != 0 {
values[3] = value
} else {
values[i] = value
}
}
if p.alpha == 1 && values[3] == 0 {
return [4]uint16{}
}
return values
}
// sampleAt 读取一个分量并扩展为十六位,交错布局复用同一像素缓冲
// 入参: i 分量序号, x 横坐标, y 纵坐标
// 返回: uint16 归一化采样值
func (p *planarTIFFImage) sampleAt(i, x, y int) uint16 {
plane, sx := p.planes[0], x
if p.stride > 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<<bits-1))
}
return uint16(plane.GrayAt(sx, y).Y) * 257
case *image.Gray16:
return plane.Gray16At(sx, y).Y
default:
v, _, _, _ := plane.At(sx, y).RGBA()
return uint16(v)
}
}
// inksAt 按网点范围读取CMYK覆盖量,范围外采样按端点截断,透明度独立归一化
// 入参: x 横坐标, y 纵坐标
// 返回: [5]uint16 青、品红、黄、黑及透明度
func (p *planarTIFFImage) inksAt(x, y int) [5]uint16 {
values := [5]uint16{0, 0, 0, 0, 65535}
for i := range 4 {
value := int64(p.sampleAt(i, x, y))
low, high := int64(p.dotRange[i][0]), int64(p.dotRange[i][1])
values[i] = uint16(max(0, min(65535, (value-low)*65535/(high-low))))
}
if p.alpha != 0 {
values[4] = p.sampleAt(4, x, y)
}
return values
}
+207
View File
@@ -0,0 +1,207 @@
// 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"
"fmt"
"image"
"image/color"
"math"
)
// readTIFFColorProfile 读取主图的ICC配置,校验颜色模型和数据范围
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: *colorProfile 颜色配置,未声明时为nil, error 配置或取消错误
func readTIFFColorProfile(ctx context.Context, data []byte, layout tiffImageLayout) (*colorProfile, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.profileSize == 0 {
return nil, nil
}
at, size := uint64(layout.profileOffset), uint64(layout.profileSize)
if at+size > 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
}
+29 -4
View File
@@ -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
}
+45 -2
View File
@@ -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)
+76 -29
View File
@@ -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 {
for _, ref := range references {
page, _ := resourceValue(pages, ref.PageID)
for _, id := range ref.ObjectIDs {
if err := ctx.Err(); err != nil {
return nil, err
}
result = append(result, StructureTarget{Page: page, PageID: ref.PageID, ID: id})
}
}
for i := range node.Children {
collect(&node.Children[i])
}
}
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
}
+28 -6
View File
@@ -14,28 +14,47 @@
package ofdgo
// structureObjectVisitor 收集页面及复合图元标识,绑定校验不解码字体、图片或裁剪
import "context"
// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪
type structureObjectVisitor struct {
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 }
+4 -1
View File
@@ -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})
+31 -19
View File
@@ -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 隔离当前文档或版本修改涉及的共享文件,同步版本清单,不修改输入包或修改集