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