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
+2018 -384

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; if (!state.editing || document.body.hasAttribute("aria-busy")) return;
const index = Number(el.structureEntry.value), path = JSON.parse(el.structureNodeSelect.value || "[]"); const index = Number(el.structureEntry.value), path = JSON.parse(el.structureNodeSelect.value || "[]");
const action = index < 0 ? "create" : el.structureAction.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 })) }; 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, 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); 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"; content.className = "structure-content";
details.append(content); details.append(content);
for (const attr of node.attributes || []) appendInfoLine(content, attr.name, attr.value); 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 children = node.children || [];
const references = node.references?.length || children.some(child => child.references?.length); const references = node.references?.length || children.some(child => child.references?.length);
for (const [i, child] of children.entries()) { 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)) alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
id, charset := editorResourceID(font.attr("ID")), font.attr("Charset") id, charset := editorResourceID(font.attr("ID")), font.attr("Charset")
unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate 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 { if result[path] == nil {
result[path] = newEditorFontUsage() result[path] = newEditorFontUsage()
} }
+15 -2
View File
@@ -32,7 +32,7 @@ type compressionImages struct {
patternVisits map[compressionPatternKey]bool patternVisits map[compressionPatternKey]bool
} }
// compressionImagePlan 为有损输出分文档分析图片,按共享路径合并最大尺寸和保护条件 // compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理
// 入参: ctx 取消上下文, options 压缩配置 // 入参: ctx 取消上下文, options 压缩配置
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误 // 返回: 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) { 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) images := make(map[string]bool)
sizes := make(map[string]image.Point) sizes := make(map[string]image.Point)
blocked := make(map[string]bool) 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 { if err := ctx.Err(); err != nil {
return nil, nil, err return nil, nil, err
} }
index := offset
if isolated {
index = (r.documentIndex + offset) % r.DocumentCount()
}
document := r document := r
if index != r.DocumentIndex() { if index != r.DocumentIndex() {
var err error var err error
@@ -80,6 +86,13 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
} }
for _, img := range catalog { for _, img := range catalog {
name := cleanPackagePath(img.Location) 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 { if old, ok := images[name]; !ok || old {
images[name] = !masks[name] images[name] = !masks[name]
} }
+38 -4
View File
@@ -126,25 +126,59 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
} }
// outputImageWorkingSet 按原图尺寸估算解码和重采样缓冲,不以降采样目标代替原图 // outputImageWorkingSet 按原图尺寸估算解码和重采样缓冲,不以降采样目标代替原图
// 按两份16位RGBA像素缓冲分批,不代表实际内存上限;格式或尺寸不明时单独处理 // 通常预留两份16位RGBA缓冲,TIFF另计分量、瓦片及蒙版存储;格式或尺寸不明时单独处理
// 入参: ctx 取消上下文, data 原始图像编码 // 入参: ctx 取消上下文, data 原始图像编码
// 返回: int 编码及像素缓冲的估算字节数,上限为单批预算 // 返回: int 编码及像素缓冲的估算字节数,上限为单批预算
func outputImageWorkingSet(ctx context.Context, data []byte) int { func outputImageWorkingSet(ctx context.Context, data []byte) int {
if len(data) >= outputImageWorkingSetLimit { if len(data) >= outputImageWorkingSetLimit {
return outputImageWorkingSetLimit return outputImageWorkingSetLimit
} }
input := imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx} input := bytes.NewReader(data)
config, _, err := decodeImageConfig(input) 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 { if err != nil || config.Width <= 0 || config.Height <= 0 {
return outputImageWorkingSetLimit return outputImageWorkingSetLimit
} }
available := outputImageWorkingSetLimit - len(data)
if config.Width > available/16/config.Height { if config.Width > available/16/config.Height {
return outputImageWorkingSetLimit return outputImageWorkingSetLimit
} }
return len(data) + config.Width*config.Height*16 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 等待整批结束后记录更小候选,失败图片保留原始编码 // runOutputImages 等待整批结束后记录更小候选,失败图片保留原始编码
// 入参: jobs 独占图片任务, results 本次写出的候选缓存 // 入参: jobs 独占图片任务, results 本次写出的候选缓存
// 返回: error 取消错误 // 返回: error 取消错误
+29 -7
View File
@@ -50,7 +50,7 @@ func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader,
} }
// compressResourceReferences 优化资源,复用自产页面引用并流式读取暂存页面 // compressResourceReferences 优化资源,复用自产页面引用并流式读取暂存页面
// 图片优化结果仅在本次写出内复用,失败时保留原始编码 // 版本文件隔离当前入口,其他文档仅优化未受版本保护的资源;结果仅在本次写出内复用
// 入参: parts 输出改动, reader 原包, removed 已移除条目, generated 本次自产页面及引用 // 入参: parts 输出改动, reader 原包, removed 已移除条目, generated 本次自产页面及引用
// 返回: error 读取或取消错误 // 返回: error 读取或取消错误
func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Reader, removed map[string]bool, generated map[string]editorGeneratedReferences) 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 return nil
} }
generated = e.generatedVectorReferences(parts, generated) 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 { if err != nil {
return err return err
} }
@@ -107,7 +107,22 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if err != nil || !safe { if err != nil || !safe {
return err 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 resourceFile struct{ key, role string }
type resourceScope struct {
role string
selected bool
}
type resourceSize struct { type resourceSize struct {
role string role string
size uint64 size uint64
@@ -147,6 +162,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
return err return err
} }
key := editorResourceLocation(name, root.attr("BaseLoc"), value) key := editorResourceLocation(name, root.attr("BaseLoc"), value)
if selected != nil && !selected[cleanPackagePath(name)] && selected[key] {
continue
}
if preserved || field == "MediaFile" && node.attr("Type") != "Image" { if preserved || field == "MediaFile" && node.attr("Type") != "Image" {
refs.files[key] = true refs.files[key] = true
continue continue
@@ -174,7 +192,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
reserved[key] = true reserved[key] = true
} }
formats := make(map[string]string) 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) imageResults := make(map[outputImageKey][]byte)
imageSources := make(map[string]outputImageSource) imageSources := make(map[string]outputImageSource)
ordered := slices.Sorted(maps.Keys(actual)) ordered := slices.Sorted(maps.Keys(actual))
@@ -321,18 +339,19 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
} }
} }
} }
if seen[role] == nil { scope := resourceScope{role, selected[key]}
seen[role] = make(map[[32]byte]string) if seen[scope] == nil {
seen[scope] = make(map[[32]byte]string)
} }
hash := source.key.hash hash := source.key.hash
if first, ok := seen[role][hash]; ok { if first, ok := seen[scope][hash]; ok {
aliases[key] = first aliases[key] = first
removed[key] = true removed[key] = true
if locations[key] != actual[key] { if locations[key] != actual[key] {
delete(parts, locations[key]) delete(parts, locations[key])
} }
} else { } 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 return err
} }
key := editorResourceLocation(name, root.attr("BaseLoc"), value) key := editorResourceLocation(name, root.attr("BaseLoc"), value)
if selected != nil && !selected[cleanPackagePath(name)] && selected[key] {
continue
}
location := locations[key] location := locations[key]
if first, ok := aliases[key]; ok { if first, ok := aliases[key]; ok {
location = locations[first] location = locations[first]
+11 -6
View File
@@ -23,20 +23,21 @@ import (
) )
// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度 // optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度
// 主图或单个瓦片的预计缓冲超限时保留原编码
// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求 // 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求
// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误 // 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误
func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]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 { if err != nil {
return data, ctx.Err() return data, ctx.Err()
} }
pixelSize := int64(8) pixelSize := int64(layout.decodedPixelBytes())
if layout.planar {
pixelSize = 16
}
if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) { if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) {
return data, ctx.Err() 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) density, ok := tiffCompressionDensity(data, layout)
if !ok { if !ok {
return data, ctx.Err() return data, ctx.Err()
@@ -73,7 +74,11 @@ func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity,
value = uint32(order.Uint16(entry[8:])) value = uint32(order.Uint16(entry[8:]))
} }
switch tag { 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: case 254:
if value != 0 { if value != 0 {
return density, false return density, false
+5 -10
View File
@@ -66,6 +66,7 @@ type Editor struct {
source *editorSource source *editorSource
origins map[string]*editorObjectOrigin origins map[string]*editorObjectOrigin
removedPages map[string]bool removedPages map[string]bool
structureRemaps map[string][]map[string]string
outlines []byte outlines []byte
encryption *encryptionState encryption *encryptionState
output *outputOptimization output *outputOptimization
@@ -82,6 +83,7 @@ type editorResource struct {
composite string composite string
vectorKey [32]byte vectorKey [32]byte
vectorStates map[string]editorCompositeState vectorStates map[string]editorCompositeState
objectIDs map[string]string
references []string references []string
usage *editorVectorUsage usage *editorVectorUsage
subset *editorFontSubset subset *editorFontSubset
@@ -120,9 +122,7 @@ func (e *Editor) SetFontFS(fsys ...fs.FS) {
// 入参: compiler 页面编译器 // 入参: compiler 页面编译器
func (e *Editor) SetPageCompiler(compiler PageCompiler) { func (e *Editor) SetPageCompiler(compiler PageCompiler) {
e.backends.Compiler = compiler e.backends.Compiler = compiler
if e.fontRenderer != nil { e.fontRenderer = nil
WithRenderBackends(e.backends)(e.fontRenderer)
}
} }
// SetRenderBackends 设置完整后端组合,字体能力变化时更新度量,不改变文档或撤销历史 // SetRenderBackends 设置完整后端组合,字体能力变化时更新度量,不改变文档或撤销历史
@@ -130,15 +130,10 @@ func (e *Editor) SetPageCompiler(compiler PageCompiler) {
func (e *Editor) SetRenderBackends(backends RenderBackends) { func (e *Editor) SetRenderBackends(backends RenderBackends) {
fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts) fontsChanged := !sameBackend(e.backends.Fonts, backends.Fonts)
if !sameBackend(e.backends.FontResources, backends.FontResources) { if !sameBackend(e.backends.FontResources, backends.FontResources) {
e.fonts = make(map[string]*FontResource) e.clearFontResourceCache()
for i := range e.resources {
e.resources[i].subset = nil
}
} }
e.backends = backends e.backends = backends
if e.fontRenderer != nil { e.fontRenderer = nil
WithRenderBackends(backends)(e.fontRenderer)
}
if fontsChanged { if fontsChanged {
e.fontMetrics = make(map[string]FontMetrics) 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)) wanted := make(map[string]bool, len(ids))
for _, id := range 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) return fmt.Errorf("missing or duplicate annotation ID %q", id)
} }
wanted[id] = true wanted[key] = true
} }
if len(wanted) == 0 { if len(wanted) == 0 {
return nil return nil
@@ -212,7 +213,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi
continue continue
} }
count++ count++
id := node.attr("ID") id := editorResourceID(node.attr("ID"))
if !wanted[id] { if !wanted[id] {
continue continue
} }
@@ -262,7 +263,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi
parts[file] = editorPatchXML(content, changes) parts[file] = editorPatchXML(content, changes)
} }
for _, id := range ids { for _, id := range ids {
if !found[id] { if !found[editorResourceID(id)] {
return fmt.Errorf("annotation %q not found", 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 { if len(parts) == 0 {
return nil 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 return texts, nil
} }
// annotationXML 读取唯一注解的独立原文,不以类型、大小或可见性筛选 // annotationXML 按ST_ID数值读取唯一注解的原文,不以类型、大小或可见性筛选
// 入参: index 页面索引, id 注解标识 // 入参: index 页面索引, id 注解标识
// 返回: []byte 注解XML, error 错误信息 // 返回: []byte 注解XML, error 错误信息
func (e *Editor) annotationXML(index int, id string) ([]byte, error) { func (e *Editor) annotationXML(index int, id string) ([]byte, error) {
if index < 0 || index >= len(e.pages) { if index < 0 || index >= len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", index) 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 == "" { if e.source == nil || e.source.document.Annotations == "" {
return nil, fmt.Errorf("page has no 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 return nil, err
} }
for _, node := range page.children { for _, node := range page.children {
if !node.matchesOFD("Annot") || node.attr("ID") != id { if !node.matchesOFD("Annot") || editorResourceID(node.attr("ID")) != key {
continue continue
} }
if result != nil { if result != nil {
@@ -521,7 +525,7 @@ func (e *Editor) annotationScope(page int, path ObjectPath) (*Reader, *Renderer,
if err != nil { if err != nil {
return nil, nil, nil, nil, err 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() reader, err := e.Reader()
if err != nil { if err != nil {
return nil, nil, nil, nil, err 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 { if statesByID == nil {
statesByID = make(map[string]map[string]editorCompositeState) 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 e.source.annotationStates = statesByID
} }
return err 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)} selection := &AnnotationSelection{editor: e, document: e.documentIdentity(), states: make(map[string]map[string]editorCompositeState)}
seen := make(map[string]bool) seen := make(map[string]bool)
for _, id := range ids { for _, id := range ids {
if seen[id] { key := editorResourceID(id)
if seen[key] {
return nil, fmt.Errorf("duplicate annotation ID %q", id) return nil, fmt.Errorf("duplicate annotation ID %q", id)
} }
seen[id] = true seen[key] = true
data, err := e.annotationXML(page, id) data, err := e.annotationXML(page, id)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -58,7 +59,7 @@ func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelectio
return nil, fmt.Errorf("annotation contains unsupported identifiers") return nil, fmt.Errorf("annotation contains unsupported identifiers")
} }
selection.data = append(selection.data, data) 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) { if e.originalPage(page) {
origin := e.source.pages[e.pages[page].ID] origin := e.source.pages[e.pages[page].ID]
@@ -106,7 +107,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx,
return err return err
} }
id := ids[editorResourceID(root.attr("ID"))] 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) data, err = editorXMLRemapIDs(data, root, ids)
if err != nil { if err != nil {
return err return err
+11 -2
View File
@@ -70,6 +70,7 @@ type editorCompositeNode struct {
patches []editorXMLPatch patches []editorXMLPatch
index int index int
states map[string]editorCompositeState states map[string]editorCompositeState
remaps []map[string]string
} }
// editorCompositeState 保存内部对象的会话排版及可还原裁剪,不写入OFD // editorCompositeState 保存内部对象的会话排版及可还原裁剪,不写入OFD
@@ -828,7 +829,9 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
return err return err
} }
if path.Annotation != "" { if path.Annotation != "" {
return e.writeAnnotationScope(page, path.Annotation, data, root.states) return e.remapStructureObjects(page, root.remaps, func() error {
return e.writeAnnotationScope(page, path.Annotation, data, root.states)
})
} }
next, err := newEditorCompositeNode(data) next, err := newEditorCompositeNode(data)
if err != nil { if err != nil {
@@ -842,7 +845,9 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
next.node.parent = origin.node.parent next.node.parent = origin.node.parent
updated.page = origin.page updated.page = origin.page
} }
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated}) return e.remapStructureObjects(page, root.remaps, func() error {
return e.updateObjectOrigins(page, []GraphicObject{next.object}, true, map[string]*editorObjectOrigin{path.ID: updated})
})
} }
// transformCompositeMember 将页面变换换算至成员坐标,保留边框及旧式边界约定 // transformCompositeMember 将页面变换换算至成员坐标,保留边框及旧式边界约定
@@ -983,6 +988,7 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er
} }
if changed { if changed {
patches = append(patches, editorXMLPatch{child.span.start, child.span.end, data}) patches = append(patches, editorXMLPatch{child.span.start, child.span.end, data})
node.remaps = append(node.remaps, child.remaps...)
removeCompositeStates(states, child.node) removeCompositeStates(states, child.node)
maps.Copy(states, child.states) maps.Copy(states, child.states)
} }
@@ -996,10 +1002,12 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er
return nil, false, err return nil, false, err
} }
if dirty { if dirty {
node.remaps = append(node.remaps, node.ref.remaps...)
id, err := e.addCompositeResource(fragment, node.ref.states) id, err := e.addCompositeResource(fragment, node.ref.states)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
node.remaps = append(node.remaps, e.resources[len(e.resources)-1].objectIDs)
root, err := parseEditorXML(data) root, err := parseEditorXML(data)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -1049,6 +1057,7 @@ func (e *Editor) addCompositeResource(data []byte, states map[string]editorCompo
return "", err return "", err
} }
resource.states = remapCompositeStates(states, ids) resource.states = remapCompositeStates(states, ids)
resource.objectIDs = ids
e.resources = append(e.resources, resource) e.resources = append(e.resources, resource)
return id, nil return id, nil
} }
+6 -4
View File
@@ -47,7 +47,7 @@ func (e *Editor) PasteCompositeSelection(page int, selection *CompositeSelection
return nil, err return nil, err
} }
var result []string 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 var err error
result, err = e.CopyObjects(page, objects, dx, dy) result, err = e.CopyObjects(page, objects, dx, dy)
return err 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) { 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 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 var err error
result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy) result, err = e.CopyObjectsToComposite(page, path, objects, dx, dy)
return err return err
@@ -198,7 +198,7 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com
// pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源 // pasteCompositeSelection 固定内部快照的页面几何和外观,粘贴失败时回收新增绘制资源
// 入参: selection 独立内部快照, paste 目标范围粘贴操作 // 入参: selection 独立内部快照, paste 目标范围粘贴操作
// 返回: error 错误信息 // 返回: 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) { if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) {
return fmt.Errorf("composite selection belongs to another document or editor") 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() defer reader.Close()
renderer := e.newRenderer(reader) renderer := e.newRenderer(reader)
var objects []GraphicObject var objects []GraphicObject
var remaps []map[string]string
for _, source := range selection.nodes { for _, source := range selection.nodes {
copy, err := newEditorCompositeNode(source.data) copy, err := newEditorCompositeNode(source.data)
if err != nil { 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} object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object}
objects = append(objects, object) objects = append(objects, object)
remaps = append(remaps, copy.remaps...)
} }
return paste(objects) return paste(objects, remaps)
} }
// preserveActionRegions 将默认点击范围固定到对象坐标,保留动作原文及显式区域 // preserveActionRegions 将默认点击范围固定到对象坐标,保留动作原文及显式区域
+20 -5
View File
@@ -63,6 +63,7 @@ func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int)
if err != nil { if err != nil {
return err return err
} }
nodes[0].owner.remaps = append(nodes[0].owner.remaps, group.remaps...)
for i, node := range nodes { for i, node := range nodes {
var replacement []byte var replacement []byte
if i == 0 { if i == 0 {
@@ -119,9 +120,11 @@ func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode
hash.Write(content) hash.Write(content)
key := [32]byte(hash.Sum(nil)) key := [32]byte(hash.Sum(nil))
var resource string var resource string
var ids map[string]string
for _, candidate := range e.resources { for _, candidate := range e.resources {
if candidate.vectorKey == key && maps.Equal(candidate.vectorStates, states) { if candidate.vectorKey == key && maps.Equal(candidate.vectorStates, states) {
resource = candidate.composite resource = candidate.composite
ids = candidate.objectIDs
break break
} }
} }
@@ -136,12 +139,17 @@ func (e *Editor) vectorInstance(renderer *Renderer, nodes []*editorCompositeNode
} }
added := &e.resources[len(e.resources)-1] added := &e.resources[len(e.resources)-1]
added.vectorKey, added.vectorStates = key, maps.Clone(states) 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) 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 { if err != nil {
return nil, err return nil, err
} }
return newEditorCompositeNode(data) group, err := newEditorCompositeNode(data)
if err == nil {
group.remaps = []map[string]string{ids}
}
return group, err
} }
// UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪 // UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪
@@ -154,7 +162,8 @@ func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([
if !members[0].Capabilities.Order { if !members[0].Capabilities.Order {
return fmt.Errorf("composite container cannot be reordered") 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 var content []byte
for i, object := range objects { for i, object := range objects {
content = append(content, object.origin.data...) content = append(content, object.origin.data...)
@@ -173,7 +182,7 @@ func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([
// ungroupCompositeNode 将组合展开至所在坐标系,祖先透明度和裁剪仅由原父级应用一次 // ungroupCompositeNode 将组合展开至所在坐标系,祖先透明度和裁剪仅由原父级应用一次
// 入参: renderer 当前读取器, node 待展开组合, replace 原子替换回调 // 入参: renderer 当前读取器, node 待展开组合, replace 原子替换回调
// 返回: error 错误信息 // 返回: 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) { if !e.compositeUngroupable(node.node) {
return fmt.Errorf("group container cannot be removed without losing attributes") 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 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) ids := make(map[string]string)
if copy.ref != nil { if copy.ref != nil {
for _, object := range objects { 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} member.object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), data: member.data, node: member.node, object: member.object}
objects[i] = 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 { if err != nil {
return err return err
} }
node.remaps = append(node.remaps, e.resources[len(e.resources)-1].objectIDs)
} }
cache[key] = replacement 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 { if e.source != nil && e.documentRoot() == reader.documentRoot && current == version && e.Info.DocID == reader.OFD.DocBody[reader.documentIndex].DocInfo.DocID {
source := *e.source source := *e.source
source.reader = reader 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 e.source = &source
} else { } else {
next, err := reader.EditorContext(ctx) next, err := reader.EditorContext(ctx)
+8
View File
@@ -49,6 +49,14 @@ type editorSourceFontKey struct {
// editorSourceFontCache 在单次资源处理中复用裁剪结果,空值表示无体积收益 // editorSourceFontCache 在单次资源处理中复用裁剪结果,空值表示无体积收益
type editorSourceFontCache map[editorSourceFontKey][]byte 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 按原字体和用字复用安全子集,不缓存错误、取消结果或原始字体 // subset 按原字体和用字复用安全子集,不缓存错误、取消结果或原始字体
// 入参: ctx 取消上下文, backend 字体资源后端, data 原字体, usage 用字记录 // 入参: ctx 取消上下文, backend 字体资源后端, data 原字体, usage 用字记录
// 返回: []byte 更小的字体子集,无收益时为nil, error 裁剪或取消错误 // 返回: []byte 更小的字体子集,无收益时为nil, error 裁剪或取消错误
+3
View File
@@ -647,6 +647,9 @@ func (e *Editor) restoreFontSnapshot(state Editor) {
state = state.transactionSnapshot() state = state.transactionSnapshot()
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
state.serial, state.revision = e.serial, e.revision 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.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
state.fontRenderer, state.fontSourcesCache = nil, nil state.fontRenderer, state.fontSourcesCache = nil, nil
+89 -12
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"io" "io"
"maps" "maps"
@@ -83,21 +84,12 @@ func (e *Editor) compactPackageFonts(ctx context.Context, parts map[string][]byt
continue continue
} }
seen[name] = true seen[name] = true
input, err := view.openFile(name) candidate, err := view.fontCleanupCandidate(ctx, name)
if err != nil { if err != nil {
return err return err
} }
var header [4]byte if candidate {
n, err := io.ReadFull(input, header[:]) candidates[name] = true
input.Close()
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
if n == len(header) {
switch string(header[:]) {
case "\x00\x01\x00\x00", "OTTO", "ttcf":
candidates[name] = true
}
} }
} }
} }
@@ -122,6 +114,91 @@ func (e *Editor) compactPackageFonts(ctx context.Context, parts map[string][]byt
return nil 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 按文档作用域收集用字,合并共享字体需求并保留字形编号 // subsetPackageFonts 按文档作用域收集用字,合并共享字体需求并保留字形编号
// 入参: ctx 取消上下文, view 独立输出快照, removed 待删除条目, generated 本次编码的页面引用 // 入参: ctx 取消上下文, view 独立输出快照, removed 待删除条目, generated 本次编码的页面引用
// 返回: error 读取、裁剪或取消错误 // 返回: error 读取、裁剪或取消错误
+5 -3
View File
@@ -165,7 +165,7 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
} }
defer reader.Close() defer reader.Close()
var result []string 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)) origins := make(map[string]*editorObjectOrigin, len(objects))
for i := range objects { for i := range objects {
object := &objects[i] object := &objects[i]
@@ -176,8 +176,10 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
origins[id] = &copy origins[id] = &copy
result = append(result, id) result = append(result, id)
} }
e.replaceGroup(page, indexes, objects, origins) return e.remapStructureObjects(page, remaps, func() error {
return nil e.replaceGroup(page, indexes, objects, origins)
return nil
})
}) })
return result, err return result, err
} }
+30 -6
View File
@@ -29,6 +29,7 @@ type editorChange struct {
// Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史 // Transaction 将多项修改作为一次原子操作提交,失败时保留原文档及历史
// 回调仅操作传入的编辑器,不可保留该实例供后续使用 // 回调仅操作传入的编辑器,不可保留该实例供后续使用
// 后端、字体来源和撤销配置仅用于本次回调,不随事务提交
// 入参: edit 批量修改回调 // 入参: edit 批量修改回调
// 返回: error 修改错误 // 返回: error 修改错误
func (e *Editor) Transaction(edit func(*Editor) error) 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 changed := e.revision != before.revision
e.history, e.historyIndex, e.historyLimit = before.history, before.historyIndex, before.historyLimit 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.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS
e.fontSourcesCache = before.fontSourcesCache e.fontSourcesCache = before.fontSourcesCache
if e.sameDocument(&before) { if e.sameResourceView(&before) {
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
} else { } else {
e.fontRenderer, e.fontMetrics = nil, make(map[string]FontMetrics) 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.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID)
next.origins = maps.Clone(e.origins) next.origins = maps.Clone(e.origins)
next.removedPages = maps.Clone(e.removedPages) next.removedPages = maps.Clone(e.removedPages)
next.structureRemaps = maps.Clone(e.structureRemaps)
next.fontMetrics = maps.Clone(e.fontMetrics) next.fontMetrics = maps.Clone(e.fontMetrics)
if e.source != nil { if e.source != nil {
source := *e.source source := *e.source
@@ -149,16 +154,19 @@ func (e *Editor) transactionSnapshot() Editor {
return next return next
} }
// restoreTransaction 恢复文档状态,保留历史和配置,仅在同一文档内共享资源 // restoreTransaction 恢复文档状态,保留历史和配置,仅在同一资源视图内共享缓存
// 入参: state 文档状态 // 入参: state 文档状态
func (e *Editor) restoreTransaction(state Editor) { func (e *Editor) restoreTransaction(state Editor) {
state = state.transactionSnapshot() state = state.transactionSnapshot()
state.retainDocumentResources(e) state.retainDocumentResources(e)
if !e.sameDocument(&state) { if !e.sameResourceView(&state) {
state.fontRenderer, state.fontMetrics = nil, make(map[string]FontMetrics) state.fontRenderer, state.fontMetrics = nil, make(map[string]FontMetrics)
} }
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
state.serial = max(e.serial, state.serial) 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.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.fontSourcesCache = e.fontSourcesCache state.fontSourcesCache = e.fontSourcesCache
state.validatedPaths = e.validatedPaths state.validatedPaths = e.validatedPaths
@@ -166,10 +174,26 @@ func (e *Editor) restoreTransaction(state Editor) {
*e = state *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 当前文档状态 // 入参: source 当前文档状态
func (e *Editor) retainDocumentResources(source *Editor) { func (e *Editor) retainDocumentResources(source *Editor) {
if !e.sameDocument(source) { if !e.sameResourceView(source) {
return return
} }
if len(source.resources) > len(e.resources) { 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 e.fontRenderer, e.fontMetrics = source.fontRenderer, source.fontMetrics
} }
// recordTransaction 记录事务快照,跨文档撤销前保留各文档后续注册的资源 // recordTransaction 记录事务快照,在各自资源视图中保留后续注册的资源
// 入参: before 修改前的文档状态 // 入参: before 修改前的文档状态
func (e *Editor) recordTransaction(before Editor) { func (e *Editor) recordTransaction(before Editor) {
if change := e.recordChange(); change != nil { 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 { if err := ctx.Err(); err != nil {
return 0, err return 0, err
} }
config, format, err := decodeImageConfigData(data) config, format, err := decodeImageConfigDataContext(ctx, data)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -133,7 +133,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
if err != nil { if err != nil {
return 0, err return 0, err
} }
metadata, err := imagePageMetadata(data, format, options.DPI) metadata, err := imagePageMetadataContext(ctx, data, format, options.DPI)
if err != nil { if err != nil {
return 0, err 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) { if at < 0 || at > len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", at) return nil, fmt.Errorf("page index %d out of range", at)
} }
doc, err := source.Doc() doc, err := source.DocContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1795,7 +1795,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
if err := reader.initRoot(); err != nil { if err := reader.initRoot(); err != nil {
return nil, err return nil, err
} }
doc, err := reader.Doc() doc, err := reader.DocContext(m.ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+31 -7
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"encoding/xml" "encoding/xml"
@@ -45,6 +46,8 @@ type editorSource struct {
annotationPages map[string]bool annotationPages map[string]bool
retiredAttachments map[string]bool retiredAttachments map[string]bool
removedAttachments map[string]bool removedAttachments map[string]bool
structureObjects map[string]map[string]bool
structureStarts map[int]map[string]int
} }
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用 // editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
@@ -298,9 +301,6 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) {
} }
maximum := e.maxID maximum := e.maxID
for name := range names { for name := range names {
if !strings.EqualFold(path.Ext(name), ".xml") {
continue
}
input, err := reader.openFile(name) input, err := reader.openFile(name)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -315,11 +315,35 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) {
return maximum, nil return maximum, nil
} }
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件 // editorXMLMaxID 按内容识别OFD结构并读取最大标识,跳过二进制及非OFD内容
// 入参: input XML输入流 // 入参: input 包内文件流
// 返回: int 最大标识, error 错误信息 // 返回: int 最大标识, error 错误信息
func editorXMLMaxID(input io.Reader) (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 maximum, started := 0, false
for { for {
token, err := decoder.Token() token, err := decoder.Token()
@@ -344,7 +368,7 @@ func editorXMLMaxID(input io.Reader) (int, error) {
} }
for _, attr := range node.Attr { for _, attr := range node.Attr {
if attr.Name.Space == "" && attr.Name.Local == "ID" { if attr.Name.Space == "" && attr.Name.Local == "ID" {
id, err := strconv.Atoi(attr.Value) id, err := strconv.Atoi(editorResourceID(attr.Value))
if err == nil { if err == nil {
maximum = max(maximum, id) maximum = max(maximum, id)
} }
+9 -4
View File
@@ -76,9 +76,13 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
} }
} else { } else {
name := e.packageName(packagePagePath("", page.ID)) name := e.packageName(packagePagePath("", page.ID))
bound := len(source.structureObjects[editorResourceID(page.ID)]) != 0
var scan *editorReferenceScan var scan *editorReferenceScan
if generated != nil { 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} 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) { if stage && generated != nil && !e.sourcePagePreserved(page) {
data, err := stager.encode(e, page, scan) data, err := stager.encode(e, page, scan)
@@ -89,9 +93,10 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
known := editorGeneratedReferences{staged: data} known := editorGeneratedReferences{staged: data}
if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 {
known.refs = scan.refs known.refs = scan.refs
known.objects = scan.graphicIDs
} }
generated[name] = known 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() input := data.open()
if e.output != nil { if e.output != nil {
input = imageInput{ReadCloser: input, context: e.output.ctx} input = imageInput{ReadCloser: input, context: e.output.ctx}
@@ -101,7 +106,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
if err != nil { if err != nil {
return nil, err 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 { } else {
data, err := e.sourcePageData(page, scan) data, err := e.sourcePageData(page, scan)
@@ -110,7 +115,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
} }
parts[name] = data parts[name] = data
if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 { 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} pageRefs[i] = Page{ID: page.ID, BaseLoc: "/" + name}
@@ -272,7 +277,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
return nil, err return nil, err
} }
} }
if err := e.pruneStructureReferences(parts); err != nil { if err := e.pruneStructureReferences(parts, generated); err != nil {
return nil, err return nil, err
} }
for name, data := range maps.Clone(parts) { for name, data := range maps.Clone(parts) {
+29 -16
View File
@@ -26,11 +26,12 @@ import (
// editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容 // editorVectorUsageLimit 限制单个矢量引用快照的记录数,不限制资源内容
const editorVectorUsageLimit = 4096 const editorVectorUsageLimit = 4096
// editorGeneratedReferences 保存本次输出条目及其引用,不跨写出复用 // editorGeneratedReferences 保存本次输出条目、资源引用及完整图元编号,不跨写出复用
type editorGeneratedReferences struct { type editorGeneratedReferences struct {
data []byte data []byte
refs *editorResourceRefs refs *editorResourceRefs
staged *editorStagedPage staged *editorStagedPage
objects map[string]bool
} }
// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效 // editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效
@@ -40,19 +41,20 @@ type editorVectorUsage struct {
refs *editorResourceRefs refs *editorResourceRefs
} }
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 // editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描
type editorReferenceScan struct { type editorReferenceScan struct {
refs *editorResourceRefs refs *editorResourceRefs
objects map[string]bool objects map[string]bool
name string graphicIDs map[string]bool
base string name string
stack []string base string
fonts []*editorFontUsage stack []string
text strings.Builder fonts []*editorFontUsage
resource bool text strings.Builder
capture bool resource bool
safe bool capture bool
seen bool safe bool
seen bool
} }
// matches 判断条目仍为本次编码结果,空条目与暂存占位不混用 // matches 判断条目仍为本次编码结果,空条目与暂存占位不混用
@@ -159,6 +161,10 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
} }
} }
var usage *editorFontUsage var usage *editorFontUsage
switch token.Name.Local {
case "Template", "PageRes", "CompositeObject", "CompositeGraphicUnit", "PageBlock":
s.graphicIDs = nil
}
if len(s.fonts) != 0 { if len(s.fonts) != 0 {
usage = s.fonts[len(s.fonts)-1] usage = s.fonts[len(s.fonts)-1]
if usage != nil && (s.stack[len(s.stack)-1] == "TextCode" || s.stack[len(s.stack)-1] == "Glyphs") { 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" { if attr.Name.Local != "ID" {
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource) 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, "" root, parent := token.Name.Local, ""
if len(s.stack) != 0 { 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 ctx = e.output.ctx
} }
progress = progress.withContext(ctx) progress = progress.withContext(ctx)
scope, err := reader.versionResourceFiles(ctx, parts, generated) _, scope, err := reader.versionResourceFiles(ctx, parts, generated)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -351,16 +351,16 @@ func (r *editorResourceRefs) reference(name, base, key, value string, resource b
if kind == editorReferenceNone { if kind == editorReferenceNone {
return return
} }
value = strings.TrimSpace(value)
if value == "" {
return
}
if kind == editorReferenceID { if kind == editorReferenceID {
if id := editorResourceID(value); id != "" { if id := editorResourceID(value); id != "" {
r.ids[id] = true r.ids[id] = true
} }
return return
} }
value = strings.TrimSpace(value)
if value == "" {
return
}
r.files[editorResourceLocation(name, base, value)] = true r.files[editorResourceLocation(name, base, value)] = true
r.files[cleanPackagePath(resolveResourcePath(name, base, value))] = true r.files[cleanPackagePath(resolveResourcePath(name, base, value))] = true
} }
@@ -389,7 +389,7 @@ func editorResourceReferenceKind(key string, resource bool) int {
// 入参: value 标识文本 // 入参: value 标识文本
// 返回: string 标准十进制标识,无效时为空 // 返回: string 标准十进制标识,无效时为空
func editorResourceID(value string) 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 { if err != nil || id == 0 {
return "" return ""
} }
+376 -41
View File
@@ -19,15 +19,17 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"maps"
"path" "path"
"slices"
"strings" "strings"
) )
// pruneStructureReferences 清理本次编辑删除的公文图元引用,未知标引和原有悬空引用保持原样 // pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用
// 改动进入输出快照,由既有文档及版本隔离流程维护共享文件 // 改动进入输出快照,由既有文档及版本隔离流程维护共享文件
// 入参: parts 输出覆盖条目 // 入参: parts 输出覆盖条目, generated 本次编码结果及扫描记录
// 返回: error 读取、解析或取消错误 // 返回: 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 { if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
return nil return nil
} }
@@ -35,43 +37,144 @@ func (e *Editor) pruneStructureReferences(parts map[string][]byte) error {
if e.output != nil { if e.output != nil {
ctx = e.output.ctx 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)) pages := make(map[string]bool, len(e.pages))
for _, page := range e.pages { for _, page := range e.pages {
pages[editorResourceID(page.ID)] = true pages[editorResourceID(page.ID)] = true
} }
removedPages := make(map[string]bool) removedPages := make(map[string]bool)
removedObjects := make(map[string]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 { 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] { if !pages[key] {
removedPages[key] = true removedPages[key] = true
continue continue
} }
data, changed := parts[e.source.reader.ResPath(source.ref.BaseLoc)] known := e.source.structureObjects[key]
if !changed || source.original == nil { 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 continue
} }
root, err := parseEditorXMLContext(ctx, data) if snapshot == nil {
reader := e.source.reader
files := make(map[string][]byte, len(reader.files)+len(parts))
maps.Copy(files, reader.files)
maps.Copy(files, parts)
snapshot = &Reader{Zip: reader.Zip, files: files, encryption: e.encryption, documentIndex: reader.documentIndex, selectedVersion: reader.selectedVersion}
if err := snapshot.initRootContext(ctx); err != nil {
return err
}
doc, err := snapshot.docStructureContext(ctx)
if err != nil {
return err
}
snapshotPages = make(map[string]Page, len(doc.Pages.Page))
for _, page := range doc.Pages.Page {
snapshotPages[editorResourceID(page.ID)] = page
}
}
original := make(map[string]bool)
if source != nil {
var err error
original, err = structurePageObjectIDs(ctx, e.source.reader, source.ref)
if err != nil {
return err
}
}
maps.Copy(original, known)
page := snapshotPages[key]
name := snapshot.ResPath(page.BaseLoc)
scanned := generated[name]
remaining := scanned.objects
if remaining == nil || !scanned.matches(parts[name]) {
var err error
remaining, err = structurePageObjectIDs(ctx, snapshot, page)
if err != nil {
return err
}
} else if snapshot.doc.Annotations != "" {
if _, err := snapshot.DocContext(ctx); err != nil {
return err
}
visitor := &structureObjectVisitor{objects: maps.Clone(remaining)}
renderer := NewRenderer(snapshot)
renderer.pageContext = ctx
for _, annotation := range snapshot.Annots[page.ID] {
if err := renderer.walkAnnotation(annotation, visitor); err != nil {
return err
}
}
remaining = visitor.objects
}
remainingObjects[key] = remaining
links, err := structureRemapLinks(ctx, e.structureRemaps[key])
if err != nil { if err != nil {
return err return err
} }
remaining := make(map[string]bool) related := make(map[string][]string)
var scan func(*editorXML) for from, targets := range links {
scan = func(node *editorXML) { for _, to := range targets {
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown { if err := ctx.Err(); err != nil {
return return err
} }
if id := editorResourceID(node.attr("ID")); id != "" { related[from] = append(related[from], to)
remaining[id] = true related[to] = append(related[to], from)
}
for _, child := range node.children {
scan(child)
} }
} }
scan(root) for id := range remaining {
for id := range source.nodes { if len(related[id]) != 0 {
id = editorResourceID(id) original[id] = true
if id != "" && !remaining[id] { }
}
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 { if removedObjects[key] == nil {
removedObjects[key] = make(map[string]bool) 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 return nil
} }
tags, err := e.source.reader.CustomTags() protected := make(map[string]bool)
if err != nil {
return err
}
for _, tag := range tags { 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) { if !structureOfficial(tag, tag.NameSpace) {
continue continue
} }
name := e.source.reader.ResPath(tag.FileLoc) name := e.source.reader.ResPath(tag.FileLoc)
data, ok := parts[name] data, ok := originals[name]
if !ok { if !ok {
data, err = e.source.reader.readFileViewContext(ctx, name) data, ok = parts[name]
if err != nil { if !ok {
return err data, err = e.source.reader.readFileViewContext(ctx, name)
if err != nil {
return err
}
}
originals[name] = data
}
remaps := pageRemaps
if starts := e.source.structureStarts[index]; len(starts) != 0 {
remaps = maps.Clone(pageRemaps)
for page, start := range starts {
links, err := structureRemapLinks(ctx, e.structureRemaps[page][start:])
if err != nil {
return err
}
remaps[page] = links
} }
} }
updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string) { mappedObjects := make(map[string]map[string][]string)
updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string, error) {
key := editorResourceID(page) key := editorResourceID(page)
if removedPages[key] { if removedPages[key] {
return page, nil return page, nil, nil
} }
kept := make([]string, 0, len(ids)) kept := make([]string, 0, len(ids))
for _, id := range 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) kept = append(kept, id)
} }
} }
return page, kept return page, kept, nil
}) })
if err != nil { if err != nil {
return err return err
} }
if !bytes.Equal(data, updated) { 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 parts[name] = updated
} }
} }
if len(redirects) != 0 {
return e.redirectStructureEntries(parts, redirects)
}
return nil 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字节 // rewriteStructureReferences 仅改写已识别公文命名空间的叶子引用,保留其他XML字节
// 入参: ctx 取消上下文, data 原始标引, tag 标引入口, rewrite 引用映射,空标识列表表示删除引用 // 入参: ctx 取消上下文, data 原始标引, tag 标引入口, rewrite 引用映射,空标识列表表示删除引用
// 返回: []byte 修改后的标引, error 解析或取消错误 // 返回: []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) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -152,7 +484,10 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag,
if len(ids) == 0 { if len(ids) == 0 {
return nil 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 { if len(next) == 0 {
patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
return nil return nil
@@ -190,7 +525,7 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag,
// structures 迁移所选页面的公文标引,按已迁移图元映射引用,不猜测其他行业的引用规则 // structures 迁移所选页面的公文标引,按已迁移图元映射引用,不猜测其他行业的引用规则
// 返回: []byte 标引索引条目, error 读取、解析或取消错误 // 返回: []byte 标引索引条目, error 读取、解析或取消错误
func (m *editorPageImport) structures() ([]byte, error) { func (m *editorPageImport) structures() ([]byte, error) {
tags, err := m.reader.CustomTags() tags, err := m.reader.CustomTagsContext(m.ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -204,10 +539,10 @@ func (m *editorPageImport) structures() ([]byte, error) {
return nil, err return nil, err
} }
count := 0 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) key := editorImportKey(page)
if !m.pages[key] { if !m.pages[key] {
return page, nil return page, nil, nil
} }
var mapped []string var mapped []string
for _, id := range ids { for _, id := range ids {
@@ -216,7 +551,7 @@ func (m *editorPageImport) structures() ([]byte, error) {
} }
} }
count += len(mapped) count += len(mapped)
return m.ids[key], mapped return m.ids[key], mapped, nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
+78 -28
View File
@@ -19,6 +19,7 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"maps"
"slices" "slices"
"strings" "strings"
) )
@@ -53,7 +54,8 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
if err := e.validateStructureReferences(tree, tag); err != nil { objects, err := e.validateStructureReferences(tree, tag, nil)
if err != nil {
return 0, err return 0, err
} }
base := e.source base := e.source
@@ -70,7 +72,7 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
} }
tag.SchemaLoc = "/" + name tag.SchemaLoc = "/" + name
} }
name, index, container, entries, err := editorStructureIndex(base) name, index, container, entries, err := editorStructureIndex(base, nil)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -107,7 +109,7 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
return 0, err return 0, err
} }
} }
return len(entries), e.commitAnnotationParts(base, parts) return len(entries), e.commitStructureParts(base, parts, objects, len(entries))
} }
// DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源 // DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源
@@ -117,7 +119,7 @@ func (e *Editor) DeleteStructure(index int) error {
if e.source == nil { if e.source == nil {
return fmt.Errorf("custom tag structure not found: %d", index) 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 { if err != nil {
return err return err
} }
@@ -125,7 +127,16 @@ func (e *Editor) DeleteStructure(index int) error {
return fmt.Errorf("custom tag structure not found: %d", index) return fmt.Errorf("custom tag structure not found: %d", index)
} }
node := entries[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 在指定父节点末尾添加子树,空路径表示根节点 // AddStructureNode 在指定父节点末尾添加子树,空路径表示根节点
@@ -242,16 +253,20 @@ func (e *Editor) SetStructureReferences(index int, path []int, references []Stru
} }
// editorStructureIndex 读取或准备当前文档的标引索引,保留内联入口和未知扩展 // editorStructureIndex 读取或准备当前文档的标引索引,保留内联入口和未知扩展
// 入参: base 编辑包快照 // 入参: base 编辑包快照, parts 本次输出条目,可为nil
// 返回: string 索引文件路径, []byte 原文, *editorXML 容器, []*editorXML 标引入口, error 读取错误 // 返回: 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 name := base.reader.documentRoot
if base.document.CustomTags.Path != "" { if base.document.CustomTags.Path != "" {
name = base.reader.ResPath(base.document.CustomTags.Path) name = base.reader.ResPath(base.document.CustomTags.Path)
} }
data, err := base.reader.readFile(name) data, ok := parts[name]
if err != nil { if !ok {
return "", nil, nil, nil, err var err error
data, err = base.reader.readFile(name)
if err != nil {
return "", nil, nil, nil, err
}
} }
root, err := parseEditorXML(data) root, err := parseEditorXML(data)
if err != nil { if err != nil {
@@ -341,13 +356,14 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa
if err != nil { if err != nil {
return err return err
} }
if err := e.validateStructureReferences(tree, tag); err != nil { objects, err := e.validateStructureReferences(tree, tag, snapshot)
if err != nil {
return err return err
} }
parts := make(map[string][]byte) parts := make(map[string][]byte)
for i, other := range tags { for i, other := range tags {
if i != index && e.source.reader.ResPath(other.FileLoc) == name { 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 { if err != nil {
return err return err
} }
@@ -363,13 +379,38 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa
} }
} }
parts[name] = updated 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 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码 // validateStructureReferences 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码
// 入参: root 待提交标引树, tag 标引入口 // 入参: root 待提交标引树, tag 标引入口, r 当前操作的编辑快照,为nil时按需创建
// 返回: error 页面或图元缺失错误 // 返回: map[string]map[string]bool 已校验目标, error 页面或图元缺失错误
func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag) error { func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag, r *Reader) (map[string]map[string]bool, error) {
var refs []StructureReference var refs []StructureReference
var collect func(*StructureNode, bool) error var collect func(*StructureNode, bool) error
collect = func(node *StructureNode, official bool) error { collect = func(node *StructureNode, official bool) error {
@@ -386,46 +427,55 @@ func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag)
return nil return nil
} }
if err := collect(root, structureOfficial(tag, root.Namespace)); err != nil { if err := collect(root, structureOfficial(tag, root.Namespace)); err != nil {
return err return nil, err
} }
if len(refs) == 0 { if len(refs) == 0 {
return nil return nil, nil
} }
r, err := e.Reader() if r == nil {
if err != nil { var err error
return err r, err = e.Reader()
if err != nil {
return nil, err
}
defer r.Close()
} }
defer r.Close()
doc, err := r.Doc() doc, err := r.Doc()
if err != nil { if err != nil {
return err return nil, err
} }
renderer := e.newRenderer(r) renderer := e.newRenderer(r)
pages := signaturePageNumbers(doc) pages := signaturePageNumbers(doc)
checked := make(map[int]map[string]bool) checked := make(map[int]map[string]bool)
result := make(map[string]map[string]bool)
for _, ref := range refs { for _, ref := range refs {
page, _ := resourceValue(pages, ref.PageID) page, _ := resourceValue(pages, ref.PageID)
if page == 0 { 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] objects := checked[page]
if objects == nil { if objects == nil {
content, err := r.PageContentByIndex(page - 1) content, err := r.PageContentByIndex(page - 1)
if err != nil { if err != nil {
return err return nil, err
} }
visitor := &structureObjectVisitor{objects: make(map[string]bool)} visitor := &structureObjectVisitor{objects: make(map[string]bool)}
if err := renderer.WalkPage(content, visitor); err != nil { if err := renderer.WalkPage(content, visitor); err != nil {
return err return nil, err
} }
objects = visitor.objects objects = visitor.objects
checked[page] = objects checked[page] = objects
} }
for _, id := range ref.ObjectIDs { for _, id := range ref.ObjectIDs {
if !objects[editorResourceID(id)] { 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
} }
+109 -24
View File
@@ -156,14 +156,14 @@ func decodeImageDataContext(ctx context.Context, data []byte) (img image.Image,
} }
}() }()
if isJPEGData(data) || isPNGData(data) { if isJPEGData(data) || isPNGData(data) {
img, err := newEncodedImage(data) img, err := newEncodedImageContext(ctx, data)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return img, img.format, nil return img, img.format, nil
} }
if isTIFFData(data) { if isTIFFData(data) {
layout, err := readTIFFLayout(bytes.NewReader(data)) layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil { if err != nil {
return nil, "tiff", err return nil, "tiff", err
} }
@@ -201,28 +201,61 @@ func isPNGData(data []byte) bool {
// 入参: data 图片数据 // 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息 // 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func decodeImageConfigData(data []byte) (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) { if isTIFFData(data) {
layout, err := readTIFFLayout(bytes.NewReader(data)) layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil { if err != nil {
return image.Config{}, "tiff", err return image.Config{}, "tiff", err
} }
if layout.planar && layout.samples > 1 || layout.samples == 1 && (layout.depth == 2 || layout.depth == 4) { profile, err := readTIFFColorProfile(ctx, data, layout)
img, _, err := tiffPlaneDirectories(nil, data, layout) if err != nil {
return image.Config{}, "tiff", err
}
var cfg image.Config
if layout.needsSampleView() {
img, _, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
cfg.ColorModel = img.ColorModel()
} else {
input := newTIFFDirectoryInput(data, layout.offset)
input.ctx = ctx
cfg, err = tiff.DecodeConfig(input)
if err != nil { if err != nil {
return image.Config{}, "tiff", err return image.Config{}, "tiff", err
} }
size := layout.size()
return image.Config{ColorModel: img.ColorModel(), Width: size.X, Height: size.Y}, "tiff", nil
} }
cfg, err := tiff.DecodeConfig(newTIFFDirectoryInput(data, layout.offset)) if profile != nil || layout.transparency != nil {
if err != nil { cfg.ColorModel = color.NRGBA64Model
return image.Config{}, "tiff", err
} }
size := layout.size() if layout.transparency != nil {
if _, err := readTIFFColorProfile(ctx, data, *layout.transparency); err != nil {
return image.Config{}, "tiff", err
}
if _, _, err := tiffPlaneDirectories(ctx, data, *layout.transparency); err != nil {
return image.Config{}, "tiff", err
}
}
size := layout.displaySize()
cfg.Width, cfg.Height = size.X, size.Y cfg.Width, cfg.Height = size.X, size.Y
return cfg, "tiff", nil 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 { if err == nil {
return cfg, normalizeSealType(format), 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) { if imageHighPrecision(source) {
out := image.NewNRGBA64(bounds) out := image.NewNRGBA64(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
if err := ctx.Err(); err != nil {
return nil, err
}
for x := bounds.Min.X; x < bounds.Max.X; x++ { for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
opacity := a opacity := a
if mask != nil { if mask != nil {
value, err := imageMaskAlpha(mask, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) 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)]) copy(row, src.Pix[offset:offset+len(row)])
} else { } else {
for x := bounds.Min.X; x < bounds.Max.X; x++ { 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)) out.SetNRGBA(x, y, imageNRGBAAt(source, x, y))
} }
} }
for x := 0; x < bounds.Dx(); x++ { for x := 0; x < bounds.Dx(); x++ {
if x&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
opacity := a opacity := a
if mask != nil { if mask != nil {
value, err := imageMaskAlpha(mask, maskBounds.Min.X+x, maskBounds.Min.Y+y-bounds.Min.Y) 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() bounds := mask.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
if err := ctx.Err(); err != nil {
return err
}
for x := bounds.Min.X; x < bounds.Max.X; x++ { for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
if _, err := imageMaskAlpha(mask, x, y); err != nil { if _, err := imageMaskAlpha(mask, x, y); err != nil {
return err 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) return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
} }
// imageNRGBAAt 获取图片NRGBA像素 // imageNRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量
// 入参: img 图片对象, x X坐标, y Y坐标 // 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素 // 返回: color.NRGBA NRGBA像素
func imageNRGBAAt(img image.Image, x, y int) color.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) 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 { if src, ok := img.(*image.YCbCr); ok {
r, g, b, _ := src.YCbCrAt(x, y).RGBA() 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} 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.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 读取十六位透明度,原生图像避免颜色装箱 // imageAlphaAt 读取十六位透明度,原生图像避免颜色装箱
@@ -579,15 +634,28 @@ func imageAlphaAt(img image.Image, x, y int) uint32 {
return alpha return alpha
} }
// imageHighPrecision 判断颜色模型是否保留十六位采样 // imageHighPrecision 判断颜色模型及调色板是否需要保留十六位采样
// 入参: img 图片对象 // 入参: img 图片对象
// 返回: bool 是否使用十六位颜色或透明度 // 返回: bool 是否使用十六位颜色或透明度
func imageHighPrecision(img image.Image) bool { func imageHighPrecision(img image.Image) bool {
model := img.ColorModel() 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 return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model
} }
// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱 // imageNRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱
// 入参: img 图片对象, x X坐标, y Y坐标 // 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA64 NRGBA64像素 // 返回: color.NRGBA64 NRGBA64像素
func imageNRGBA64At(img image.Image, x, y int) color.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: case *image.NRGBA:
c := src.NRGBAAt(x, y) 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} 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: case *image.Gray16:
c := src.Gray16At(x, y) c := src.Gray16At(x, y)
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff} 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.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 获取透明像素相邻的可见颜色 // transparentEdgeColor 获取透明像素相邻的可见颜色
@@ -731,6 +806,11 @@ func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image
} }
offset := y*stride + step - 1 offset := y*stride + step - 1
for x := 0; x < w; x++ { 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 { if (pixels[offset] == 0) != hasZero {
hasZero, hasVisible = true, true hasZero, hasVisible = true, true
break scan break scan
@@ -744,6 +824,11 @@ func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image
return nil, 0, err return nil, 0, err
} }
for x := bounds.Min.X; x < bounds.Max.X; x++ { 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 { if imageAlphaAt(source, x, y) == 0 {
hasZero = true hasZero = true
} else { } else {
+1 -1
View File
@@ -41,7 +41,7 @@ func decodeImageConfig(input io.Reader) (image.Config, string, error) {
if err != nil { if err != nil {
return image.Config{}, "tiff", err return image.Config{}, "tiff", err
} }
size := layout.size() size := layout.displaySize()
return image.Config{Width: size.X, Height: size.Y}, "tiff", nil return image.Config{Width: size.X, Height: size.Y}, "tiff", nil
} }
if len(prefix) == 34 && string(prefix[:2]) == "BM" { if len(prefix) == 34 && string(prefix[:2]) == "BM" {
+21 -6
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
@@ -27,20 +28,27 @@ type imagePageInfo struct {
xdpi, ydpi float64 xdpi, ydpi float64
} }
// imagePageMetadata 读取图片方向与密度,TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF // imagePageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消
// 入参: data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI // TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
// 返回: imagePageInfo 方向与横纵DPI, error 元数据错误 // 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, error) { // 返回: imagePageInfo 方向与横纵DPI, error 元数据或取消错误
func imagePageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (imagePageInfo, error) {
info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi} info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi}
if err := ctx.Err(); err != nil {
return info, err
}
var exif []byte var exif []byte
var directory uint32 var directory uint32
scaleX, scaleY := 1.0, 1.0
if format == "tiff" { if format == "tiff" {
layout, err := readTIFFLayout(bytes.NewReader(data)) layout, err := readTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil { if err != nil {
return info, err return info, err
} }
directory = layout.offset directory = layout.offset
exif = data 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 { if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 {
x := int32(binary.LittleEndian.Uint32(data[38:42])) x := int32(binary.LittleEndian.Uint32(data[38:42]))
@@ -51,6 +59,9 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
} }
if format == "png" { if format == "png" {
for offset := 8; offset+12 <= len(data); { for offset := 8; offset+12 <= len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
n := uint64(binary.BigEndian.Uint32(data[offset:])) n := uint64(binary.BigEndian.Uint32(data[offset:]))
if n > uint64(len(data)-offset-12) { if n > uint64(len(data)-offset-12) {
return info, fmt.Errorf("truncated PNG chunk") return info, fmt.Errorf("truncated PNG chunk")
@@ -81,6 +92,9 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
} }
} else if format == "jpeg" { } else if format == "jpeg" {
for offset := 2; offset < len(data); { for offset := 2; offset < len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
if data[offset] != 0xff { if data[offset] != 0xff {
return info, fmt.Errorf("invalid JPEG marker") return info, fmt.Errorf("invalid JPEG marker")
} }
@@ -140,7 +154,8 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
info.orientation = 1 info.orientation = 1
} }
} }
return info, nil info.xdpi, info.ydpi = info.xdpi*scaleX, info.ydpi*scaleY
return info, ctx.Err()
} }
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录 // imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
+97 -21
View File
@@ -48,6 +48,13 @@ type encodedImageResult struct {
err error err error
} }
// resampledImageMask 按替换图尺寸读取已校验的二值蒙版,不分配缩放像素
type resampledImageMask struct {
source image.Image
original image.Rectangle
bounds image.Rectangle
}
// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据 // NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据
// 入参: data 原始编码 // 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误
@@ -236,30 +243,58 @@ func (r *Renderer) imageObjectMask(object ImageObject, target image.Image) (imag
if err != nil { if err != nil {
return nil, err return nil, err
} }
bounds := mask.Bounds() return &resampledImageMask{source: mask, original: mask.Bounds(), bounds: image.Rectangle{Max: size}}, ctx.Err()
result := image.NewGray(image.Rectangle{Max: size}) }
for y := range size.Y {
if err := ctx.Err(); err != nil { // Bounds 返回替换图对应的蒙版范围
return nil, err // 返回: image.Rectangle 像素区域
} func (s *resampledImageMask) Bounds() image.Rectangle { return s.bounds }
sy := bounds.Min.Y + int((int64(y)*2+1)*int64(bounds.Dy())/(2*int64(size.Y)))
for x := range size.X { // ColorModel 返回二值蒙版的灰度颜色模型
sx := bounds.Min.X + int((int64(x)*2+1)*int64(bounds.Dx())/(2*int64(size.X))) // 返回: color.Model 灰度模型
value, err := imageMaskAlpha(mask, sx, sy) func (s *resampledImageMask) ColorModel() color.Model { return color.GrayModel }
if err != nil {
return nil, err // GrayAt 按像素中心读取原蒙版,区域外返回黑色
} // 入参: x 横坐标, y 纵坐标
result.SetGray(x, y, color.Gray{Y: value}) // 返回: color.Gray 二值灰度
} func (s *resampledImageMask) GrayAt(x, y int) color.Gray {
if !image.Pt(x, y).In(s.bounds) {
return color.Gray{}
} }
return result, ctx.Err() bounds, size := s.original, s.bounds.Size()
sx := bounds.Min.X + int((uint64(x-s.bounds.Min.X)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X))
sy := bounds.Min.Y + int((uint64(y-s.bounds.Min.Y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y))
return imageGrayAt(s.source, sx, sy)
}
// At 返回采样后的二值灰度
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (s *resampledImageMask) At(x, y int) color.Color { return s.GrayAt(x, y) }
// RGBA64At 返回不透明的十六位灰度颜色,供蒙版合成直接读取
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 像素颜色
func (s *resampledImageMask) RGBA64At(x, y int) color.RGBA64 {
value := uint16(s.GrayAt(x, y).Y) * 257
return color.RGBA64{R: value, G: value, B: value, A: 65535}
} }
// newEncodedImage 接管编码数据并读取头信息 // newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码 // 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*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 { if err != nil {
return nil, err return nil, err
} }
@@ -291,7 +326,7 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) {
return img, nil return img, nil
} }
// imageResourceCost 预留十六位像素空间,并计入编码、像素缓冲及补边源图的可见容量 // imageResourceCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
// 入参: img 图片资源, limit 缓存预算 // 入参: img 图片资源, limit 缓存预算
// 返回: int 估算字节数或超预算标记 // 返回: int 估算字节数或超预算标记
func imageResourceCost(img image.Image, limit int) 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) return cost + pixels + cap(encoded.data)
} }
var buffers [4]int var buffers [5]int
switch source := img.(type) { switch source := img.(type) {
case *image.RGBA: case *image.RGBA:
buffers[0] = cap(source.Pix) 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 buffers[0], buffers[1] = cap(source.Pix), cap(source.Palette)*16
case *image.YCbCr: 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: 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: case *transparentEdgeImage:
buffers[0] = imageResourceCost(source.source, limit-cost) buffers[0] = imageResourceCost(source.source, limit-cost)
case *transparentEdgeImage64: case *transparentEdgeImage64:
@@ -350,6 +423,9 @@ func imageResourceCost(img image.Image, limit int) int {
} }
storage += size storage += size
} }
if pixels > limit-cost {
return limit + 1
}
return cost + max(pixels, storage) return cost + max(pixels, storage)
} }
+94 -15
View File
@@ -28,18 +28,26 @@ import (
// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 // errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var errTIFFRandomAccess = errors.New("TIFF directory requires random access") var errTIFFRandomAccess = errors.New("TIFF directory requires random access")
// tiffImageLayout 保存单页TIFF的尺寸、方向、采样及压缩参数 // tiffImageLayout 保存TIFF目录的图像布局及关联透明蒙版
type tiffImageLayout struct { type tiffImageLayout struct {
width, height int width, height int
tileWidth int
tileHeight int
orientation int orientation int
offset, next uint32 offset, next uint32
reduced, mask bool reduced, mask bool
planar bool planar bool
auxiliary bool
samples uint32 samples uint32
depth uint32 depth uint32
fillOrder uint32 fillOrder uint32
compression uint32 compression uint32
predictor uint32
photometric uint32
profileOffset uint32
profileSize uint32
t4, t6 uint32 t4, t6 uint32
transparency *tiffImageLayout
} }
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源 // 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") return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
} }
// readTIFFLayout 选择单页TIFF的主图,允许缩略图目录并拒绝循环和多个主图 // readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流 // 入参: input 从文件头开始的输入流
// 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误 // 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (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 var result tiffImageLayout
if err := ctx.Err(); err != nil {
return result, err
}
var header [8]byte var header [8]byte
if _, err := io.ReadFull(input, header[:]); err != nil { if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err return result, err
@@ -81,18 +99,23 @@ func readTIFFLayout(input io.Reader) (tiffImageLayout, error) {
offset := int64(order.Uint32(header[4:])) offset := int64(order.Uint32(header[4:]))
position := int64(8) position := int64(8)
var seen map[uint32]bool var seen map[uint32]bool
var transparency *tiffImageLayout
for offset != 0 { for offset != 0 {
if seen != nil && seen[uint32(offset)] { if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain") 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 { if err != nil {
return result, err return result, err
} }
if layout.mask { if layout.mask {
return result, fmt.Errorf("TIFF transparency mask directories are not supported") if !layout.reduced {
} if transparency != nil {
if !layout.reduced || layout.next == 0 && seen == nil { return result, fmt.Errorf("multiple TIFF transparency masks")
}
transparency = &layout
}
} else if !layout.reduced || layout.next == 0 && seen == nil {
if result.offset != 0 { if result.offset != 0 {
return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources") 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 offset, position = int64(layout.next), end
} }
if err := ctx.Err(); err != nil {
return tiffImageLayout{}, err
}
if result.offset == 0 { if result.offset == 0 {
return result, fmt.Errorf("missing TIFF main image") 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 return result, nil
} }
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素 // readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: input 输入流, order 字节序, offset 目录偏移, position 当前流位置 // 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构或读取错误 // 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误
func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) { func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) {
var result tiffImageLayout var result tiffImageLayout
if err := ctx.Err(); err != nil {
return result, position, err
}
var header [4]byte var header [4]byte
if offset < 8 { if offset < 8 {
return result, position, fmt.Errorf("invalid TIFF directory offset") 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 { if offset < position {
return result, position, errTIFFRandomAccess 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 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.orientation, result.offset, result.samples, result.fillOrder = 1, uint32(offset), 1, 1
result.depth = 1 result.depth = 1
result.compression = 1 result.compression = 1
result.predictor = 1
var entry [12]byte var entry [12]byte
var previous uint16 var previous uint16
var subfile, legacy uint32 var subfile, legacy uint32
var hasSubfile bool var hasSubfile bool
var invalidMaskDepth bool
for i := range count { for i := range count {
if err := ctx.Err(); err != nil {
return result, position, err
}
if _, err := io.ReadFull(input, entry[:]); err != nil { if _, err := io.ReadFull(input, entry[:]); err != nil {
return result, position, err 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") return result, position, fmt.Errorf("invalid TIFF directory order")
} }
previous = tag previous = tag
if tag == 258 && kind == 3 && number == 1 { if tag == 338 {
result.depth = uint32(order.Uint16(entry[8:10])) 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 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") return result, position, fmt.Errorf("invalid TIFF image layout")
} }
value := uint64(order.Uint32(entry[8:12])) value := uint64(order.Uint32(entry[8:12]))
if kind == 3 { if kind == 3 {
value = uint64(order.Uint16(entry[8:10])) 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") return result, position, fmt.Errorf("invalid TIFF dimensions")
} }
switch tag { switch tag {
@@ -181,6 +236,7 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position
case 259: case 259:
result.compression = uint32(value) result.compression = uint32(value)
case 262: case 262:
result.photometric = uint32(value)
result.mask = value == 4 result.mask = value == 4
case 266: case 266:
if value != 1 && value != 2 { if value != 1 && value != 2 {
@@ -206,6 +262,12 @@ func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position
result.t4 = uint32(value) result.t4 = uint32(value)
case 293: case 293:
result.t6 = uint32(value) 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 { 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 { if result.width == 0 || result.height == 0 {
return result, position, fmt.Errorf("missing TIFF dimensions") 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 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) 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 返回从零开始的显示区域 // Bounds 返回从零开始的显示区域
// 返回: image.Rectangle 显示区域 // 返回: image.Rectangle 显示区域
func (s *orientedTIFFImage) 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} { for _, tag := range []uint16{258, 317, 339} {
v, err := value(tag, 1) 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) return nil, fmt.Errorf("invalid TIFF fax sample field %d", tag)
} }
} }
photo, err := value(262, 2) 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") return nil, fmt.Errorf("invalid TIFF fax color model")
} }
width := uint32(layout.width) width := uint32(layout.width)
@@ -150,7 +150,7 @@ func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (im
params := pdfgo.Dictionary{ params := pdfgo.Dictionary{
"K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows), "K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows),
"EndOfBlock": pdfgo.Boolean(false), "EndOfLine": pdfgo.Boolean(layout.compression == 3), "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}} 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) 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)}
}
+337 -67
View File
@@ -33,34 +33,73 @@ type tiffPlaneInput struct {
ctx context.Context ctx context.Context
} }
// planarTIFFImage 组合独立分量或映射调色板,不额外分配交错像素缓冲 // planarTIFFImage 组合颜色与Alpha分量,保留交错步长并按需映射调色板
type planarTIFFImage struct { type planarTIFFImage struct {
planes []image.Image planes []image.Image
bounds image.Rectangle bounds image.Rectangle
depth uint32 depth uint32
packed [4]uint32 packed [5]uint32
alpha uint32 offsets [5]int
gray bool alpha uint32
palette []color.NRGBA64 gray bool
cmyk bool
dotRange [4][2]uint16
palette []color.NRGBA64
stride int
invert bool
predictorWidth int
} }
// decodeTIFFImage 按存储方式解码主图并统一显示方向 // needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局 // 返回: bool 是否需要分量视图
// 返回: image.Image 图像, error 解码或取消错误 func (layout tiffImageLayout) needsSampleView() bool {
func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) { 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)
if layout.compression >= 2 && layout.compression <= 4 { }
pixels, err := decodeTIFFFax(ctx, data, layout)
if err == nil && layout.orientation != 1 { // decodedPixelBytes 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量
pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout} // 返回: uint64 每像素预算字节数
func (layout tiffImageLayout) decodedPixelBytes() uint64 {
samples := uint64(layout.samples)
if layout.planar {
if layout.photometric == 5 && samples > 4 {
return 20
} }
return pixels, err return 16
} }
ranges, err := tiffBitOrderRanges(ctx, data, layout) if !layout.needsSampleView() {
return 8
}
bytes := uint64(1)
if layout.depth == 2 || layout.depth == 4 {
bytes = uint64(layout.depth)
} else if layout.depth > 8 {
bytes = 2
}
return max(uint64(8), samples*bytes)
}
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局
// 返回: image.Image 图像, error 解码或取消错误
func decodeTIFFPixels(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) {
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil { if err != nil {
return nil, err 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 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) result, directories, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -74,6 +113,11 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (
if err != nil { if err != nil {
return nil, err 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) result.planes = append(result.planes, plane)
} }
pixels = result pixels = result
@@ -86,6 +130,12 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (
return nil, err return nil, err
} }
} }
if profile != nil {
pixels, err = applyTIFFColorProfile(ctx, pixels, profile)
if err != nil {
return nil, err
}
}
if ctx != nil { if ctx != nil {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
@@ -97,7 +147,7 @@ func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (
return pixels, nil return pixels, nil
} }
// tiffPlaneDirectories 为颜色、透明度或调色板索引构建单通道TIFF目录 // tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局 // 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误 // 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误
func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayout) (*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) return tiffPlaneValue(data, order, entry, 0)
} }
samples, err := value(277, 1) 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") return nil, nil, fmt.Errorf("invalid TIFF plane count")
} }
photo, err := value(262, math.MaxUint32) 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") return nil, nil, fmt.Errorf("unsupported TIFF planar color model")
} }
compression, err := value(259, 1) compression, err := value(259, 1)
@@ -143,22 +193,55 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
return nil, nil, err return nil, nil, err
} }
fill, err := value(266, 1) 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") return nil, nil, fmt.Errorf("unsupported TIFF plane fill order")
} }
predictor, err := value(317, 1) predictor, err := value(317, 1)
if err != nil || predictor != 1 && predictor != 2 { if err != nil || predictor != 1 && predictor != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF plane predictor") 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) colors := uint32(3)
if photo != 2 { if result.cmyk {
colors = 4
} else if photo != 2 {
colors = 1 colors = 1
} }
if samples != colors { if samples != colors {
result.alpha, err = value(338, 0) entry := entries[338]
if err != nil || samples != colors+1 || result.alpha != 1 && result.alpha != 2 { if samples < colors || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples-colors {
return nil, nil, fmt.Errorf("unsupported TIFF extra planes") 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 { } else if entries[338] != nil {
return nil, nil, fmt.Errorf("unexpected TIFF extra samples") 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 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") return nil, nil, fmt.Errorf("too many TIFF plane blocks")
} }
for _, tag := range []uint16{locations, lengths} { for _, tag := range []uint16{locations, lengths} {
entry := entries[tag] 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") 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 ctx != nil {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, nil, err return nil, nil, err
@@ -213,7 +296,21 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
} }
} }
var directories [][]byte var directories [][]byte
var firstDepth uint32
for plane := uint32(0); plane < samples; plane++ { 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) depth := uint32(1)
if entry := entries[258]; entry != nil { if entry := entries[258]; entry != nil {
depth, err = tiffPlaneValue(data, order, entry, plane) 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 { if err != nil || depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 {
return nil, nil, fmt.Errorf("unsupported TIFF plane depth") 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) result.depth = max(result.depth, depth)
if depth < 8 && predictor != 1 { if result.cmyk && plane < 4 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane predictor") 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 { if depth == 2 || depth == 4 {
result.packed[plane] = depth result.packed[slot] = 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 { factor *= uint64(depth)
return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit")
}
if compression == 2 || compression == 3 || compression == 4 { if compression == 2 || compression == 3 || compression == 4 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane compression") 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 { if entry := entries[339]; entry != nil {
format, err := tiffPlaneValue(data, order, entry, plane) format, err := tiffPlaneValue(data, order, entry, plane)
if err != nil || format != 1 { if err != nil || format != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF sample format") return nil, nil, fmt.Errorf("unsupported TIFF sample format")
} }
} }
if photo == 3 { if photo == 3 && plane == 0 {
entry := entries[320] entry := entries[320]
if depth > 8 || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != 3<<depth { 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") 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} 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) directory := make([]byte, 2, 6+count*12)
for i := range count { for i := range count {
if ctx != nil { 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]) entry := bytes.Clone(data[offset+2+i*12 : offset+14+i*12])
tag := order.Uint16(entry) tag := order.Uint16(entry)
if tag == 338 || tag == 320 { if tag == 338 || tag == 320 || tag >= 332 && tag <= 337 || tag == 34675 {
continue continue
} }
scalar := uint32(0) scalar := uint32(0)
switch tag { switch tag {
case 258: case 258:
scalar = depth scalar = depth
if result.packed[plane] != 0 { if result.packed[slot] != 0 {
scalar = 1 scalar = 1
} }
case 256, 322: case 256, 322:
if result.packed[plane] != 0 { if factor != 1 {
original, err := tiffPlaneValue(data, order, entry, 0) original, err := tiffPlaneValue(data, order, entry, 0)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
order.PutUint16(entry[2:], 4) order.PutUint16(entry[2:], 4)
order.PutUint32(entry[4:], 1) order.PutUint32(entry[4:], 1)
order.PutUint32(entry[8:], original*depth) order.PutUint32(entry[8:], original*uint32(factor))
} }
case 262: case 262:
scalar = 1 scalar = 1
if result.gray && plane == 0 && photo == 0 {
scalar = 0
}
case 277, 284, 339: case 277, 284, 339:
scalar = 1 scalar = 1
case 317:
if predictor == 2 && (result.stride > 1 || depth < 8) {
order.PutUint16(entry[8:], 1)
}
case locations, lengths: case locations, lengths:
entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks)) entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks))
if err != nil { if err != nil {
@@ -318,6 +473,64 @@ func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayo
return result, directories, nil 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数组中的一项,校验完整数组范围 // tiffPlaneValue 读取SHORT或LONG数组中的一项,校验完整数组范围
// 入参: data 原编码, order 字节序, entry 目录项, index 元素序号 // 入参: data 原编码, order 字节序, entry 目录项, index 元素序号
// 返回: uint32 元素值, error 类型或越界错误 // 返回: uint32 元素值, error 类型或越界错误
@@ -452,37 +665,94 @@ func (p *planarTIFFImage) components(x, y int) [4]uint16 {
if !image.Pt(x, y).In(p.bounds) { if !image.Pt(x, y).In(p.bounds) {
return [4]uint16{} return [4]uint16{}
} }
values := [4]uint16{0, 0, 0, 65535} if p.cmyk {
for i, plane := range p.planes { ink := p.inksAt(x, y)
var value uint16 maximum := uint32(65535)
switch plane := plane.(type) { if p.alpha == 1 {
case *image.Gray: maximum = uint32(ink[4])
if bits := p.packed[i]; bits != 0 { if maximum == 0 {
for bit := range int(bits) { return [4]uint16{}
value = value<<1 | uint16(plane.GrayAt(x*int(bits)+bit, y).Y>>7)
}
value *= 65535 / uint16(1<<bits-1)
} else {
value = uint16(plane.GrayAt(x, y).Y) * 257
} }
case *image.Gray16:
value = plane.Gray16At(x, y).Y
default:
v, _, _, _ := plane.At(x, y).RGBA()
value = uint16(v)
} }
if len(p.palette) != 0 { 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}
count := 3
if p.gray || len(p.palette) != 0 {
count = 1
}
if p.alpha != 0 {
count++
}
for i := range count {
value := p.sampleAt(i, x, y)
if i == 0 && p.invert {
value = 65535 - value
}
if len(p.palette) != 0 && i == 0 {
c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535] c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535]
return [4]uint16{c.R, c.G, c.B, c.A} values = [4]uint16{c.R, c.G, c.B, c.A}
} } else if p.gray && i == 0 {
if p.gray && i == 0 {
values[0], values[1], values[2] = value, value, value values[0], values[1], values[2] = value, value, value
} else if p.gray { } else if p.gray || len(p.palette) != 0 {
values[3] = value values[3] = value
} else { } else {
values[i] = value 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 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读取 // 返回的文件路径可直接传入ResData读取
// 返回: []CustomTag 自定义标引列表, error 错误信息 // 返回: []CustomTag 自定义标引列表, error 错误信息
func (r *Reader) CustomTags() ([]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 { if err != nil {
return nil, err return nil, err
} }
if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil { if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil {
var customTags CustomTags var customTags CustomTags
partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags) partPath, err := r.readDocumentPartContext(ctx, doc.CustomTags.Path, &customTags)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for i := range customTags.CustomTag { for i := range customTags.CustomTag {
if err := ctx.Err(); err != nil {
return nil, err
}
tag := &customTags.CustomTag[i] tag := &customTags.CustomTag[i]
tag.SchemaLoc = r.resourceLocation(partPath, "", tag.SchemaLoc) tag.SchemaLoc = r.resourceLocation(partPath, "", tag.SchemaLoc)
tag.FileLoc = r.resourceLocation(partPath, "", tag.FileLoc) tag.FileLoc = r.resourceLocation(partPath, "", tag.FileLoc)
@@ -566,6 +577,9 @@ func (r *Reader) CustomTags() ([]CustomTag, error) {
if customTags.CustomTag == nil { if customTags.CustomTag == nil {
customTags.CustomTag = []CustomTag{} customTags.CustomTag = []CustomTag{}
} }
if err := ctx.Err(); err != nil {
return nil, err
}
doc.CustomTags.CustomTag = customTags.CustomTag doc.CustomTags.CustomTag = customTags.CustomTag
} }
return doc.CustomTags.CustomTag, nil return doc.CustomTags.CustomTag, nil
@@ -1124,13 +1138,24 @@ func (r *Reader) resolvePageAreaContext(ctx context.Context, area PageArea, temp
// 入参: loc 文件位置, value 文档结构 // 入参: loc 文件位置, value 文档结构
// 返回: string 文件路径, error 错误信息 // 返回: string 文件路径, error 错误信息
func (r *Reader) readDocumentPart(loc string, value any) (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)) fullPath := r.ResPath(strings.TrimSpace(loc))
data, err := r.readFile(fullPath) data, err := r.readFileViewContext(ctx, fullPath)
if err != nil { if err != nil {
return "", err 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) return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
} }
if err := ctx.Err(); err != nil {
return "", err
}
return fullPath, nil 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 return img
} }
for x := bounds.Min.X; x < bounds.Max.X; x++ { 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) pixel := imageNRGBA64At(img, x, y)
alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y)
opaque = opaque && alpha == 65535 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) mask, ok := result.(*image.RGBA)
if !ok { if !ok {
mask = image.NewRGBA(image.Rect(0, 0, w, h)) 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{}) { if mask.Rect.Min != (image.Point{}) {
normalized := *mask normalized := *mask
normalized.Rect = mask.Rect.Sub(mask.Rect.Min) normalized.Rect = mask.Rect.Sub(mask.Rect.Min)
mask = &normalized 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 { if err := work.Err(); err != nil {
r.renderError = err r.renderError = err
return img 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] row := out.Pix[y*out.Stride : y*out.Stride+w*4]
pixels := src.Pix[offset : offset+w*4] pixels := src.Pix[offset : offset+w*4]
for x := 0; x < len(row); x += 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]) copy(row[x:x+3], pixels[x:x+3])
row[x+3] = uint8(int(pixels[x+3]) * int(row[x+3]) / 255) 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 return img
} }
for x := bounds.Min.X; x < bounds.Max.X; x++ { 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) c := imageNRGBAAt(source, x, y)
a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
c.A = uint8(int(c.A) * int(a) / 255) c.A = uint8(int(c.A) * int(a) / 255)
+77 -30
View File
@@ -41,6 +41,7 @@ type Structure struct {
} }
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML // StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
// References仅解释公文标引中的ObjectRef;未知扩展保留结构,不推定其引用语义 // References仅解释公文标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
type StructureNode struct { type StructureNode struct {
Name string `json:"name"` Name string `json:"name"`
@@ -88,38 +89,48 @@ func (r *Reader) StructureContext(ctx context.Context) ([]Structure, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
tags, err := r.CustomTags() tags, err := r.CustomTagsContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := make([]Structure, len(tags)) result := make([]Structure, len(tags))
for i, tag := range tags { for i, tag := range tags {
item := &result[i] item, err := r.readStructureContext(ctx, tag)
*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)
}
}
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
} }
if err != nil { if err != nil {
item.Error = err.Error() item.Error = err.Error()
} }
result[i] = item
} }
return result, nil 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规则识别公文引用 // parseStructure 解析有界XML结构,按GB/T33476.3的ObjectRef规则识别公文引用
// 入参: ctx 取消上下文, data 标引XML, tag 标引入口 // 入参: ctx 取消上下文, data 标引XML, tag 标引入口
// 返回: *StructureNode 根节点, error 解析、限额或取消错误 // 返回: *StructureNode 根节点, error 解析、限额或取消错误
func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*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 root *StructureNode
var stack []*StructureNode var stack []*StructureNode
var text []*strings.Builder
count := 0 count := 0
for { for {
if err := ctx.Err(); err != nil { 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 { if len(stack) >= 128 || count > 100000 {
return nil, fmt.Errorf("custom tag structure limit exceeded") 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} node := StructureNode{Name: token.Name.Local, Namespace: token.Name.Space}
for _, attr := range token.Attr { for _, attr := range token.Attr {
if attr.Name.Space != "xmlns" && !(attr.Name.Space == "" && attr.Name.Local == "xmlns") { 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: case xml.CharData:
if len(stack) > 0 { if len(stack) > 0 {
stack[len(stack)-1].Text += string(token) text[len(stack)-1].Write(token)
} else if strings.TrimSpace(string(token)) != "" { } else if strings.Trim(string(token), " \t\r\n") != "" {
return nil, fmt.Errorf("text outside custom tag root") return nil, fmt.Errorf("text outside custom tag root")
} }
case xml.EndElement: case xml.EndElement:
node := stack[len(stack)-1] 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) official := structureOfficial(tag, root.Namespace)
for _, parent := range stack { for _, parent := range stack {
official = official && parent.Namespace == root.Namespace 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) return r.StructureTargetsContext(context.Background(), index, path)
} }
// StructureTargetsContext 在当前文档和版本内解析标引目标,不搜索其他文档的同名图元 // StructureTargetsContext 仅读取所选标引,在当前文档和版本内定位目标,不搜索其他文档的同名图元
// 入参: ctx 取消上下文, index 标引清单索引, path 子节点索引路径 // 入参: ctx 取消上下文, index 标引清单索引, path 子节点索引路径
// 返回: []StructureTarget 有序目标, error 解析、渲染或取消错误 // 返回: []StructureTarget 有序目标, error 解析、渲染或取消错误
func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path []int) ([]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 { if err != nil {
return nil, err 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) 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 { for _, i := range path {
if err := ctx.Err(); err != nil {
return nil, err
}
if i < 0 || i >= len(node.Children) { if i < 0 || i >= len(node.Children) {
return nil, fmt.Errorf("custom tag node not found") return nil, fmt.Errorf("custom tag node not found")
} }
node = &node.Children[i] 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) doc, err := r.Reader.DocContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pages := signaturePageNumbers(doc) pages := signaturePageNumbers(doc)
var result []StructureTarget var result []StructureTarget
var collect func(*StructureNode) for _, ref := range references {
collect = func(node *StructureNode) { page, _ := resourceValue(pages, ref.PageID)
for _, ref := range node.References { for _, id := range ref.ObjectIDs {
for _, id := range ref.ObjectIDs { if err := ctx.Err(); err != nil {
page, _ := resourceValue(pages, ref.PageID) return nil, err
result = append(result, StructureTarget{Page: page, PageID: ref.PageID, ID: id})
} }
} 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) byPage := make(map[int]map[string]*StructureTarget)
for i := range result { for i := range result {
if err := ctx.Err(); err != nil {
return nil, err
}
target := &result[i] target := &result[i]
if target.Page == 0 { if target.Page == 0 {
target.Error = "custom tag page not found" 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 byPage[target.Page][editorResourceID(target.ID)] = target
target.Error = "custom tag object not found" target.Error = "custom tag object not found"
} }
view := *r
view.RenderAnnotations, view.pageContext = true, ctx
for page, targets := range byPage { for page, targets := range byPage {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
@@ -285,12 +329,15 @@ func (r *Renderer) StructureTargetsContext(ctx context.Context, index int, path
if err != nil { if err != nil {
return nil, err return nil, err
} }
visitor := &structureVisitor{ctx: ctx, renderer: r, targets: targets} visitor := &structureVisitor{ctx: ctx, renderer: &view, targets: targets}
if err := r.WalkPage(content, visitor); err != nil { if err := view.WalkPage(content, visitor); err != nil {
return nil, err return nil, err
} }
} }
for i, target := range result { for i, target := range result {
if err := ctx.Err(); err != nil {
return nil, err
}
if source := byPage[target.Page][editorResourceID(target.ID)]; source != nil { if source := byPage[target.Page][editorResourceID(target.ID)]; source != nil {
result[i] = *source result[i] = *source
} }
+30 -8
View File
@@ -14,28 +14,47 @@
package ofdgo package ofdgo
// structureObjectVisitor 收集页面及复合图元标识,绑定校验不解码字体、图片或裁剪 import "context"
// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪
type structureObjectVisitor struct { type structureObjectVisitor struct {
objects map[string]bool objects map[string]bool
targets []StructureTarget targets []StructureTarget
listTargets bool
} }
// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容和复合图元成员 // StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观
// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次 // 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次
// 入参: page 从0开始的页面索引 // 入参: page 从0开始的页面索引
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误 // 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误
func (r *Renderer) StructureObjects(page int) ([]StructureTarget, 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 { if err != nil {
return nil, err return nil, err
} }
visitor := &structureObjectVisitor{objects: make(map[string]bool)} visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true}
if err := r.WalkPage(content, visitor); err != nil { view := *r
view.RenderAnnotations = true
view.pageContext = ctx
if err := view.WalkPage(content, visitor); err != nil {
return nil, err return nil, err
} }
for i := range visitor.targets { 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 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 return visitor.targets, nil
} }
@@ -46,6 +65,9 @@ func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool {
id := editorResourceID(editorObjectID(*object)) id := editorResourceID(editorObjectID(*object))
if id != "" && !v.objects[id] { if id != "" && !v.objects[id] {
v.objects[id] = true v.objects[id] = true
if !v.listTargets {
return false
}
target := StructureTarget{ID: id} target := StructureTarget{ID: id}
if object.Type == "TextObject" { if object.Type == "TextObject" {
target.Text = object.TextObject.Text() target.Text = object.TextObject.Text()
@@ -58,7 +80,7 @@ func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool {
// EndObject 满足分组访问接口,不维护分组栈 // EndObject 满足分组访问接口,不维护分组栈
func (v *structureObjectVisitor) EndObject() {} func (v *structureObjectVisitor) EndObject() {}
// BeginAnnotation 不为注解容器分配正文图元标识 // BeginAnnotation 不将注释容器编号作为图元标识
// 入参: id 注解标识 // 入参: id 注解标识
// 返回: bool 始终为false // 返回: bool 始终为false
func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return 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 return err
} }
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}} 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: ""}) 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 { for _, attr := range node.Attributes {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value}) 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 取消上下文 // 入参: ctx 取消上下文
// 返回: map[string]bool 受保护路径, error 读取或结构错误 // 返回: map[string]bool 受保护路径, error 读取或结构错误
func (r *Reader) versionSharedFiles(ctx context.Context) (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) shared := make(map[string]bool)
data, err := r.readFileViewContext(ctx, "OFD.xml") data, err := r.readFileViewContext(ctx, "OFD.xml")
if err != nil { if err != nil {
@@ -163,17 +185,6 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
shared[name] = true 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) versions, err := r.DocumentVersionsContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -204,21 +215,22 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
return shared, nil return shared, nil
} }
// versionResourceFiles 限定当前版本的资源扫描范围,合并其他文档依赖以保护跨文档引用 // versionResourceFiles 分别记录当前入口依赖与完整扫描范围,其他文档和版本清单用于保护共享引用
// 入参: ctx 取消上下文, parts 待写入条目, generated 本次自产页面及引用 // 入参: ctx 取消上下文, parts 待写入条目, generated 本次自产页面及引用
// 返回: map[string]bool 扫描路径,非版本文档返回nil, 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, 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 { if r == nil || !r.OFD.DocBody[r.documentIndex].versioned {
return nil, nil return nil, nil, nil
} }
entry, err := r.DocumentRootContext(ctx) entry, err := r.DocumentRootContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
files, err := r.versionFileDependencies(ctx, entry, parts, generated) files, err := r.versionFileDependencies(ctx, entry, parts, generated)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
selected := maps.Clone(files)
if r.versionInfo != nil { if r.versionInfo != nil {
for _, file := range r.versionInfo.Files { for _, file := range r.versionInfo.Files {
files[file.Location] = true 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...) other, err := r.documentFiles(ctx, indexes...)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
maps.Copy(files, other) maps.Copy(files, other)
return files, nil return selected, files, nil
} }
// versionChanges 隔离当前文档或版本修改涉及的共享文件,同步版本清单,不修改输入包或修改集 // versionChanges 隔离当前文档或版本修改涉及的共享文件,同步版本清单,不修改输入包或修改集