feat(渲染编辑): 优化渲染编辑完善资源保存
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-05 12:49:51 +08:00
1 parent 5dcd7a4e8d
commit 6c14eef1ee
39 files changed
+1926 -420

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261004151751-bff53238d953
github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004151751-bff53238d953 h1:4fz7oPfhMZr8N2fWjv7Gn6+L2TInWGarQm3UGaqV+54=
github.com/xiaoqidun/pdfgo v0.0.0-20261004151751-bff53238d953/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143 h1:OELHUuQnFJHk9qyHQX2gzdMvSQNbnEyD70YeB5Eoldg=
github.com/xiaoqidun/pdfgo v0.0.0-20261005044242-0007b05b8143/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+18 -2
View File
@@ -32,6 +32,13 @@ import (
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
// 返回: error 读取或取消错误
func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) error {
return e.compressResourceReferences(parts, reader, removed, nil)
}
// 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 {
if e.output == nil || e.output.options.Mode == CompressionUnchanged || e.output.protected {
return nil
}
@@ -68,9 +75,18 @@ func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader,
return nil
}
actual[key] = name
data, replaced := parts[name]
if known, ok := generated[name]; ok && replaced && known.refs != nil && known.matches(data) {
refs.merge(known.refs)
continue
}
var input io.ReadCloser
if data, ok := parts[name]; ok {
input = io.NopCloser(bytes.NewReader(data))
if replaced {
if known := generated[name]; data == nil && known.staged != nil {
input = known.staged.open()
} else {
input = io.NopCloser(bytes.NewReader(data))
}
} else {
var err error
input, err = reader.openFile(name)
+39 -33
View File
@@ -45,30 +45,31 @@ const editorPathValidationLimit = 1024
// 阶段可重复,准备完成不代表保存成功
// 回调不可重入修改编辑器,可返回context.Canceled等调用方停止原因
type Editor struct {
OnWriteProgress func(stage string, completed, total int) error
Info DocInfo
pages []PageContent
resources []editorResource
fonts map[string]*FontResource
fontDirs []string
fontFS []fs.FS
fontRenderer *Renderer
fontMetrics map[string]FontMetrics
backends RenderBackends
images map[string]image.Point
resourceID map[editorResourceKey]string
maxID int
history []editorChange
historyIndex int
historyLimit int
revision uint64
serial uint64
source *editorSource
origins map[string]*editorObjectOrigin
removedPages map[string]bool
outlines []byte
encryption *encryptionState
output *outputOptimization
OnWriteProgress func(stage string, completed, total int) error
Info DocInfo
pages []PageContent
resources []editorResource
fonts map[string]*FontResource
fontDirs []string
fontFS []fs.FS
fontRenderer *Renderer
fontSourcesCache *fontSourceCache
fontMetrics map[string]FontMetrics
backends RenderBackends
images map[string]image.Point
resourceID map[editorResourceKey]string
maxID int
history []editorChange
historyIndex int
historyLimit int
revision uint64
serial uint64
source *editorSource
origins map[string]*editorObjectOrigin
removedPages map[string]bool
outlines []byte
encryption *encryptionState
output *outputOptimization
}
// editorResource 文档内嵌资源
@@ -100,6 +101,7 @@ type editorResourceKey struct {
func (e *Editor) SetFontDirs(dirs ...string) {
e.fontDirs = slices.Clone(dirs)
e.fontRenderer = nil
e.fontSourcesCache = nil
e.fontMetrics = make(map[string]FontMetrics)
}
@@ -108,6 +110,7 @@ func (e *Editor) SetFontDirs(dirs ...string) {
func (e *Editor) SetFontFS(fsys ...fs.FS) {
e.fontFS = slices.Clone(fsys)
e.fontRenderer = nil
e.fontSourcesCache = nil
e.fontMetrics = make(map[string]FontMetrics)
}
@@ -687,7 +690,12 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
// 入参: reader 当前文档快照
// 返回: *Renderer 度量器
func (e *Editor) newRenderer(reader *Reader) *Renderer {
return NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
if e.fontSourcesCache == nil {
e.fontSourcesCache = newFontSourceCache()
}
renderer := NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
renderer.fontSourcesCache = e.fontSourcesCache.child()
return renderer
}
// definition 获取独立定义资源的标识
@@ -1091,14 +1099,12 @@ func (e *Editor) prepareText(obj *TextObject) error {
if obj.VScale != 0 || obj.Decoration != "" {
return &EditError{Code: EditUnsupportedObject, Err: fmt.Errorf("text extensions are not supported for creation")}
}
if external {
if len(obj.CGTransform) != 0 {
return fmt.Errorf("external font text cannot use glyph indices")
}
} else {
if err := validateTextGlyphs(*obj, sfnt.NumGlyphs()); err != nil {
return err
}
glyphCount := 1 << 16
if !external {
glyphCount = int(sfnt.NumGlyphs())
}
if err := validateTextGlyphs(*obj, glyphCount); err != nil {
return err
}
if !finite(obj.HScale) || obj.HScale < 0 || !finite(obj.LineWidth) || obj.LineWidth < 0 || !finite(obj.MiterLimit) || obj.MiterLimit < 0 {
return fmt.Errorf("invalid text dimensions")
+81 -3
View File
@@ -155,13 +155,85 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra
for end < len(prepared) && e.copiedLayerStyle(objects[end]) == style {
end++
}
layers[target].Objects = append(layers[target].Objects, prepared[i:end]...)
if len(layers[target].Objects) == 0 {
layers[target].Objects = prepared[i:end:end]
} else {
layers[target].Objects = append(layers[target].Objects, prepared[i:end]...)
}
i = end
}
e.replaceLayers(page, layers)
e.replacePreparedLayers(page, layers)
return result, nil
}
// appendOwnedObjects 接收本库生成对象的所有权,全部校验后提交一次撤销记录
// 入参: ctx 取消上下文, page 目标页, objects 无原文和标识且调用方不再修改的对象
// 返回: error 校验或取消错误,失败时不提交页面内容
func (e *Editor) appendOwnedObjects(ctx context.Context, page int, objects []GraphicObject) error {
if err := ctx.Err(); err != nil {
return err
}
if _, err := e.page(page); err != nil {
return err
}
if len(objects) == 0 {
return nil
}
if err := e.prepareSourceIDs(); err != nil {
return err
}
validation := &editorValidation{Editor: e}
for i, object := range objects {
if err := ctx.Err(); err != nil {
return err
}
if object.origin != nil || editorObjectID(object) != "" {
return fmt.Errorf("owned objects must not have identifiers or original content")
}
prepared, err := validation.prepareObject("", object)
if err != nil {
return err
}
objects[i] = prepared
}
if err := ctx.Err(); err != nil {
return err
}
for i := range objects {
id := strconv.Itoa(e.maxID + i + 1)
switch objects[i].Type {
case "TextObject":
objects[i].TextObject.ID = id
case "PathObject":
objects[i].PathObject.ID = id
case "ImageObject":
objects[i].ImageObject.ID = id
case "CompositeObject":
objects[i].CompositeGraphicUnit.ID = id
}
}
layers := copyEditorPage(e.pages[page]).Content.Layer
target := 0
if e.originalPage(page) {
target = len(e.source.pages[e.pages[page].ID].original.Content.Layer)
}
if target < len(layers) {
target = len(layers) - 1
}
e.maxID += len(objects)
if target == len(layers) || layers[target].DrawParam != "" {
layers = append(layers, Layer{ID: e.nextID(), Type: "Body"})
target = len(layers) - 1
}
if len(layers[target].Objects) == 0 {
layers[target].Objects = objects[:len(objects):len(objects)]
} else {
layers[target].Objects = append(layers[target].Objects, objects...)
}
e.replacePreparedLayers(page, layers)
return nil
}
// PageObjectInfo 批量获取页面对象能力与位置,不重复扫描同一容器
// 每次按当前页面计算,变更和撤销后无需失效旧缓存
// 入参: page 页面索引
@@ -543,12 +615,18 @@ func (e *Editor) replaceLayers(page int, layers []Layer) {
return
}
after := copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
e.replacePreparedLayers(page, after)
}
// replacePreparedLayers 提交已隔离的图层容器,历史启用时仍保留独立快照
// 入参: page 页面索引, after 已准备且不再由调用方修改的图层列表
func (e *Editor) replacePreparedLayers(page int, after []Layer) {
if e.historyLimit == 0 {
e.pages[page].Content.Layer = after
e.recordChange()
return
}
before = copyEditorPage(PageContent{Content: Content{Layer: before}}).Content.Layer
before := copyEditorPage(e.pages[page]).Content.Layer
apply := func(e *Editor, layers []Layer) {
e.pages[page].Content.Layer = copyEditorPage(PageContent{Content: Content{Layer: layers}}).Content.Layer
}
+123
View File
@@ -0,0 +1,123 @@
// 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 (
"reflect"
"sync"
)
// editorClonePlans 仅共享类型的可变字段索引,不持有文档对象或资源
var editorClonePlans sync.Map
// cloneEditorData 深复制编辑快照,未导出的排版及原文来源保持不可变共享
// 入参: value 原值
// 返回: T 独立副本
func cloneEditorData[T any](value T) T {
source := reflect.ValueOf(value)
if !source.IsValid() {
return value
}
return cloneEditorValue(source).Interface().(T)
}
// cloneEditorValue 复制公开指针、切片及数组,纯值数据不创建额外结构
// 入参: value 原值
// 返回: reflect.Value 独立副本
func cloneEditorValue(value reflect.Value) reflect.Value {
if !value.IsValid() || !editorCloneMutable(value.Type()) {
return value
}
switch value.Kind() {
case reflect.Pointer:
if value.IsNil() {
return value
}
result := reflect.New(value.Type().Elem())
cloneEditorInto(result.Elem(), value.Elem())
return result
case reflect.Slice:
if value.IsNil() {
return value
}
result := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
if !editorCloneMutable(value.Type().Elem()) {
reflect.Copy(result, value)
} else {
for i := range value.Len() {
cloneEditorInto(result.Index(i), value.Index(i))
}
}
return result
default:
result := reflect.New(value.Type()).Elem()
cloneEditorInto(result, value)
return result
}
}
// cloneEditorInto 直接填充目标结构,避免指针及切片成员的二次结构分配
// 入参: target 可写目标, source 原值
func cloneEditorInto(target, source reflect.Value) {
switch source.Kind() {
case reflect.Struct:
target.Set(source)
for _, i := range editorCloneFields(source.Type()) {
cloneEditorInto(target.Field(i), source.Field(i))
}
case reflect.Array:
target.Set(source)
if editorCloneMutable(source.Type().Elem()) {
for i := range source.Len() {
cloneEditorInto(target.Index(i), source.Index(i))
}
}
default:
target.Set(cloneEditorValue(source))
}
}
// editorCloneMutable 判断类型是否含有需要隔离的公开可变成员
// 入参: kind 数据类型
// 返回: bool 是否需要独立复制
func editorCloneMutable(kind reflect.Type) bool {
switch kind.Kind() {
case reflect.Pointer, reflect.Slice:
return true
case reflect.Array:
return editorCloneMutable(kind.Elem())
case reflect.Struct:
return len(editorCloneFields(kind)) != 0
}
return false
}
// editorCloneFields 按类型复用公开可变字段,索引发布后保持只读
// 入参: kind 结构类型
// 返回: []int 可变字段索引
func editorCloneFields(kind reflect.Type) []int {
if cached, ok := editorClonePlans.Load(kind); ok {
return cached.([]int)
}
var fields []int
for i := range kind.NumField() {
field := kind.Field(i)
if field.IsExported() && editorCloneMutable(field.Type) {
fields = append(fields, i)
}
}
actual, _ := editorClonePlans.LoadOrStore(kind, fields)
return actual.([]int)
}
+2
View File
@@ -49,6 +49,7 @@ 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
e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS
e.fontSourcesCache = before.fontSourcesCache
if e.sameDocument(&before) {
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
} else {
@@ -159,6 +160,7 @@ func (e *Editor) restoreTransaction(state Editor) {
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
state.serial = max(e.serial, state.serial)
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.fontSourcesCache = e.fontSourcesCache
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
*e = state
}
-39
View File
@@ -912,45 +912,6 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
return size, nil
}
// cloneEditorData 深复制编辑快照,未导出的排版及原文来源保持不可变共享
// 入参: value 原值
// 返回: T 独立副本
func cloneEditorData[T any](value T) T {
return cloneEditorValue(reflect.ValueOf(value)).Interface().(T)
}
// cloneEditorValue 复制结构体中的可变指针和切片
// 入参: value 原值
// 返回: reflect.Value 独立副本
func cloneEditorValue(value reflect.Value) reflect.Value {
switch value.Kind() {
case reflect.Pointer:
if !value.IsNil() {
result := reflect.New(value.Type().Elem())
result.Elem().Set(cloneEditorValue(value.Elem()))
return result
}
case reflect.Slice:
if !value.IsNil() {
result := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
for i := range value.Len() {
result.Index(i).Set(cloneEditorValue(value.Index(i)))
}
return result
}
case reflect.Struct:
result := reflect.New(value.Type()).Elem()
result.Set(value)
for i := range value.NumField() {
if result.Field(i).CanSet() && value.Type().Field(i).IsExported() {
result.Field(i).Set(cloneEditorValue(value.Field(i)))
}
}
return result
}
return value
}
// editorObjectXML 编码可编辑对象,包含独立命名空间声明
// 入参: object 对象
// 返回: []byte XML片段, error 错误信息
+94 -23
View File
@@ -31,7 +31,22 @@ import (
// 入参: progress 保存进度回调,预览时为nil
// 返回: map[string][]byte 替换及新增条目, error 错误信息
func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) {
return e.sourcePartsReferences(progress, nil)
}
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
// 入参: progress 保存进度回调, generated 本次自产页面引用,可为nil
// 返回: map[string][]byte 输出条目, error 编码错误
func (e *Editor) sourcePartsReferences(progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) {
return e.sourcePartsPrepared(progress, generated, false)
}
// sourcePartsPrepared 准备保存条目,按需压缩暂存自产页面
// 入参: progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存
// 返回: map[string][]byte 输出条目, error 编码错误
func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[string]editorGeneratedReferences, stage bool) (map[string][]byte, error) {
parts := make(map[string][]byte)
var stager editorPageStager
source := e.source
reader := source.reader
pageRefs := make([]Page, len(e.pages))
@@ -56,11 +71,31 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
}
} else {
name := e.packageName(packagePagePath("", page.ID))
data, err := e.sourceNewPageXML(page)
if err != nil {
return nil, err
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 stage && generated != nil && !e.sourcePagePreserved(page) {
data, err := stager.encode(e, page, scan)
if err != nil {
return nil, err
}
parts[name] = nil
known := editorGeneratedReferences{staged: data}
if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 {
known.refs = scan.refs
}
generated[name] = known
} else {
data, err := e.sourcePageData(page, scan)
if err != nil {
return nil, err
}
parts[name] = data
if scan != nil && scan.safe && scan.seen && len(scan.stack) == 0 {
generated[name] = editorGeneratedReferences{data: data, refs: scan.refs}
}
}
parts[name] = data
pageRefs[i] = Page{ID: page.ID, BaseLoc: "/" + name}
}
}
@@ -80,7 +115,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
})
}
if len(fonts)+len(images)+len(spaces) != 0 {
data, err := encodeOFDXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
if err != nil {
return nil, err
}
@@ -155,7 +190,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
if original != nil {
entries = append(entries, data[original.start:original.end])
} else {
item, err := encodeOFDXML(func(x *ofdXML) {
item, err := e.encodeXML(func(x *ofdXML) {
x.root("Page", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: page.ID}, {Name: xml.Name{Local: "BaseLoc"}, Value: page.BaseLoc}})
x.end("Page")
})
@@ -215,7 +250,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
parts[name] = updated
}
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 {
if err := e.prunePageReferences(parts); err != nil {
if err := e.prunePageReferences(parts, generated); err != nil {
return nil, err
}
}
@@ -225,6 +260,10 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
if actual != name {
delete(parts, name)
parts[actual] = data
if known, ok := generated[name]; ok {
delete(generated, name)
generated[actual] = known
}
}
}
}
@@ -392,11 +431,14 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
// 返回: []byte 图层XML, error 错误信息
func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
if !slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) {
data, err := encodeOFDXML(func(x *ofdXML) {
data, err := e.encodeXML(func(x *ofdXML) {
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}}
attrs.add("DrawParam", layer.DrawParam)
x.root("Layer", attrs)
for _, object := range layer.Objects {
if x.err != nil {
return
}
x.object(object, false)
}
x.end("Layer")
@@ -429,16 +471,23 @@ func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
// 入参: page 页面
// 返回: []byte 页面XML, error 错误信息
func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
preserved := false
for _, layer := range page.Content.Layer {
if slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) {
preserved = true
break
return e.sourcePageData(page, nil)
}
// sourcePageData 编码新增页面,仅在未保留原文时收集编码引用
// 入参: page 页面内容, references 本次引用记录,可为nil
// 返回: []byte 页面XML, error 编码错误
func (e *Editor) sourcePageData(page PageContent, references *editorReferenceScan) ([]byte, error) {
if !e.sourcePagePreserved(page) {
var data bytes.Buffer
x := newOFDXML(&data)
if e.output != nil {
x.ctx = e.output.ctx
}
}
if !preserved {
data, err := encodeOFDXML(func(x *ofdXML) { x.page(page) })
return bytes.TrimPrefix(data, []byte(xml.Header)), err
x.references = references
x.page(page)
err := x.finish()
return bytes.TrimPrefix(data.Bytes(), []byte(xml.Header)), err
}
area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox))
if err != nil {
@@ -458,7 +507,7 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
}
data := append(area, content...)
if len(page.Actions) != 0 {
actions, err := encodeOFDXML(func(x *ofdXML) { x.actions(page.Actions) })
actions, err := e.encodeXML(func(x *ofdXML) { x.actions(page.Actions) })
if err != nil {
return nil, err
}
@@ -467,16 +516,29 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
return editorXMLContainer("Page", nil, data)
}
// sourcePagePreserved 判断新增页面是否仍有对象原文需要保留
// 入参: page 页面内容
// 返回: bool 是否包含原文对象
func (e *Editor) sourcePagePreserved(page PageContent) bool {
for _, layer := range page.Content.Layer {
if slices.ContainsFunc(layer.Objects, func(object GraphicObject) bool { return e.objectOrigin(editorObjectID(object)) != nil }) {
return true
}
}
return false
}
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress editorProgress) (int64, error) {
parts, err := e.sourceParts(progress)
generated := make(map[string]editorGeneratedReferences)
parts, err := e.sourcePartsPrepared(progress, generated, true)
if err != nil {
return 0, err
}
maps.Copy(parts, fonts)
removed, err := e.compactSourceResources(parts, progress)
removed, err := e.compactSourceReferences(parts, progress, generated)
if err != nil {
return 0, err
}
@@ -484,7 +546,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
if removed == nil {
removed = make(map[string]bool)
}
if err := e.compressResourceParts(parts, reader, removed); err != nil {
if err := e.compressResourceReferences(parts, reader, removed, generated); err != nil {
return 0, err
}
if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
@@ -537,7 +599,11 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
}
if data, ok := remaining[name]; ok {
header := file.FileHeader
if err := e.writeOutputEntry(archive, header, data); err != nil {
if staged := generated[name].staged; data == nil && staged != nil {
if err := e.writeStagedPage(archive, header, staged); err != nil {
return output.count, err
}
} else if err := e.writeOutputEntry(archive, header, data); err != nil {
return output.count, err
}
delete(remaining, name)
@@ -575,7 +641,12 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
if strings.HasSuffix(strings.ToLower(name), ".xml") {
method = zip.Deflate
}
if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: method}, remaining[name]); err != nil {
header := zip.FileHeader{Name: name, Method: method}
if staged := generated[name].staged; remaining[name] == nil && staged != nil {
if err := e.writeStagedPage(archive, header, staged); err != nil {
return output.count, err
}
} else if err := e.writeOutputEntry(archive, header, remaining[name]); err != nil {
return output.count, err
}
}
+25 -2
View File
@@ -16,15 +16,16 @@ package ofdgo
import (
"fmt"
"io"
"maps"
"strings"
)
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
// 入参: parts 输出覆盖条目
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
// 返回: error 读取或解析错误
func (e *Editor) prunePageReferences(parts map[string][]byte) error {
func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
removed := maps.Clone(e.removedPages)
if removed == nil {
removed = make(map[string]bool)
@@ -54,6 +55,11 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
}
seen := make(map[string]bool)
for len(queue) > 0 {
if e.output != nil {
if err := e.output.ctx.Err(); err != nil {
return err
}
}
name := queue[0]
queue = queue[1:]
if seen[strings.ToLower(name)] {
@@ -67,6 +73,17 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
if err != nil {
return err
}
} else if known := generated[name]; data == nil && known.staged != nil {
input := known.staged.open()
if e.output != nil {
input = imageInput{ReadCloser: input, context: e.output.ctx}
}
var err error
data, err = io.ReadAll(input)
input.Close()
if err != nil {
return err
}
}
root, err := parseEditorXML(data)
if err != nil {
@@ -75,6 +92,11 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
var patches []editorXMLPatch
var walk func(*editorXML) error
walk = func(node *editorXML) error {
if e.output != nil {
if err := e.output.ctx.Err(); err != nil {
return err
}
}
if node.name.Space != "" && node.name.Space != ofdNamespace && node.name.Space != "http://www.ofdspec.org" {
return nil
}
@@ -153,6 +175,7 @@ func (e *Editor) prunePageReferences(parts map[string][]byte) error {
}
if len(patches) != 0 {
parts[name] = editorPatchXML(data, patches)
delete(generated, name)
}
}
return nil
+232
View File
@@ -0,0 +1,232 @@
// 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 (
"bytes"
"encoding/xml"
"maps"
"strings"
"unicode/utf8"
)
// editorGeneratedReferences 保存本次编码页面及其引用,不跨写出复用
type editorGeneratedReferences struct {
data []byte
refs *editorResourceRefs
staged *editorStagedPage
}
// matches 判断条目仍为本次编码结果,空条目与暂存占位不混用
// 入参: data 当前条目数据
// 返回: bool 是否可以复用本次引用
func (r editorGeneratedReferences) matches(data []byte) bool {
if r.staged != nil {
return data == nil
}
return bytes.Equal(data, r.data)
}
// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描
type editorReferenceScan struct {
refs *editorResourceRefs
name string
base string
stack []string
fonts []*editorFontUsage
text strings.Builder
resource bool
capture bool
safe bool
seen bool
}
// accept 共用读取和自产页面的引用判断,不将对象自身标识视为引用
// 入参: token 已解析或已编码的XML事件
// 返回: bool 是否仍可完整判断引用
func (s *editorReferenceScan) accept(token xml.Token) bool {
if !s.safe {
return false
}
switch token := token.(type) {
case xml.StartElement:
if token.Name.Space != "" && token.Name.Space != ofdNamespace {
s.safe = false
return false
}
if token.Name.Local == "CustomTags" || token.Name.Local == "Extensions" || token.Name.Local == "ExtendData" || token.Name.Local == "Data" {
s.safe = false
return false
}
if len(s.stack) == 0 {
s.seen = true
s.resource = token.Name.Local == "Res"
if s.resource {
for _, attr := range token.Attr {
if attr.Name.Local == "BaseLoc" {
s.base = attr.Value
}
}
}
}
var usage *editorFontUsage
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") {
usage.unsafe = true
}
}
for _, attr := range token.Attr {
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Local == "xmlns:ofd" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
continue
}
if attr.Name.Space != "" {
s.safe = false
return false
}
if attr.Name.Local != "ID" {
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
}
if attr.Name.Local == "Font" {
usage = s.refs.fontUsage(attr.Value)
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
usage.unsafe = true
}
} else if attr.Name.Local == "Substitution" {
s.refs.fontUsage(attr.Value).unsafe = true
}
}
s.stack = append(s.stack, token.Name.Local)
s.fonts = append(s.fonts, usage)
s.text.Reset()
s.capture = editorResourceReferenceKind(token.Name.Local, s.resource) != editorReferenceNone || usage != nil && (token.Name.Local == "TextCode" || token.Name.Local == "Glyphs")
case xml.CharData:
if s.capture {
s.text.Write(token)
}
case xml.EndElement:
if len(s.stack) == 0 || s.stack[len(s.stack)-1] != token.Name.Local {
s.safe = false
return false
}
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" {
s.refs.fontUsage(s.text.String()).unsafe = true
}
if usage := s.fonts[len(s.fonts)-1]; usage != nil {
usage.text(token.Name.Local, s.text.String())
}
s.stack = s.stack[:len(s.stack)-1]
s.fonts = s.fonts[:len(s.fonts)-1]
s.text.Reset()
s.capture = false
if len(s.stack) != 0 {
key, usage := s.stack[len(s.stack)-1], s.fonts[len(s.fonts)-1]
s.capture = editorResourceReferenceKind(key, s.resource) != editorReferenceNone || usage != nil && (key == "TextCode" || key == "Glyphs")
}
case xml.Directive:
s.safe = false
}
return s.safe
}
// merge 合并本次编码已确认的引用,不共享字体用字的可变集合
// 入参: other 独立页面引用
func (r *editorResourceRefs) merge(other *editorResourceRefs) {
maps.Copy(r.ids, other.ids)
maps.Copy(r.files, other.files)
for id, usage := range other.fonts {
r.fontUsage(id).merge(usage, false)
}
}
// referenceToken 记录自产OFD事件,编码会替换的字符使本页回到完整扫描
// 入参: token 已写入的XML事件
func (x *ofdXML) referenceToken(token xml.Token) {
if x.references == nil || x.err != nil {
return
}
s := x.references
switch value := token.(type) {
case xml.StartElement:
value.Name = xml.Name{Space: ofdNamespace, Local: strings.TrimPrefix(value.Name.Local, "ofd:")}
for _, attr := range value.Attr {
if editorResourceReferenceKind(attr.Name.Local, s.resource) != editorReferenceNone && !editorReferenceTextSafe(attr.Value) {
s.safe = false
}
}
token = value
case xml.EndElement:
value.Name = xml.Name{Space: ofdNamespace, Local: strings.TrimPrefix(value.Name.Local, "ofd:")}
token = value
case xml.CharData:
if s.capture && !editorReferenceTextSafe(string(value)) {
s.safe = false
}
}
if !s.accept(token) {
x.references = nil
}
}
// referenceElement 收集已知叶节点的引用,其他结构仍完整扫描
// 入参: name 元素名称, value 已编码的子树内容
func (x *ofdXML) referenceElement(name string, value any) {
if x.references == nil || x.err != nil {
return
}
var attrs ofdAttrs
switch value := value.(type) {
case ShdColor:
attrs.add("ColorSpace", value.ColorSpace)
case *ShdColor:
if value == nil {
return
}
attrs.add("ColorSpace", value.ColorSpace)
case *Sound:
if value == nil {
return
}
attrs.add("ResourceID", value.ResourceID)
case *Movie:
if value == nil {
return
}
attrs.add("ResourceID", value.ResourceID)
case *GotoBookmark, *URI, *GotoA:
default:
x.references.safe = false
x.references = nil
return
}
x.referenceToken(xml.StartElement{Name: xml.Name{Local: "ofd:" + name}, Attr: attrs})
x.referenceToken(xml.EndElement{Name: xml.Name{Local: "ofd:" + name}})
}
// editorReferenceTextSafe 判断XML编码是否保持引用文本的Unicode值
// 入参: value 原始引用或字体用字
// 返回: bool 是否无需字符替换
func editorReferenceTextSafe(value string) bool {
if !utf8.ValidString(value) {
return false
}
for _, char := range value {
if char != '\t' && char != '\n' && char != '\r' && (char < 0x20 || char == 0xfffe || char == 0xffff) {
return false
}
}
return true
}
+58 -81
View File
@@ -27,6 +27,12 @@ import (
"strings"
)
const (
editorReferenceNone = iota
editorReferenceID
editorReferenceFile
)
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
type editorResourceRefs struct {
ids map[string]bool
@@ -41,6 +47,13 @@ type editorResourceRefs struct {
// 入参: parts 已修改和新增的包内条目, progress 保存进度回调
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
func (e *Editor) compactSourceResources(parts map[string][]byte, progress editorProgress) (map[string]bool, error) {
return e.compactSourceReferences(parts, progress, nil)
}
// compactSourceReferences 合并自产页面的已知引用,其他条目仍完整扫描
// 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用
// 返回: map[string]bool 可移除条目, error 读取或取消错误
func (e *Editor) compactSourceReferences(parts map[string][]byte, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string]bool, error) {
if e.source.reader.DocumentCount() > 1 || e.output != nil && e.output.protected {
return nil, nil
}
@@ -68,9 +81,18 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor
if strings.HasSuffix(name, "/") {
continue
}
data, replaced := parts[name]
if known, ok := generated[name]; ok && replaced && known.refs != nil && known.matches(data) {
refs.merge(known.refs)
continue
}
var input io.ReadCloser
if data, ok := parts[name]; ok {
input = io.NopCloser(bytes.NewReader(data))
if replaced {
if known := generated[name]; data == nil && known.staged != nil {
input = known.staged.open()
} else {
input = io.NopCloser(bytes.NewReader(data))
}
} else {
var err error
input, err = reader.openFile(name)
@@ -78,8 +100,14 @@ func (e *Editor) compactSourceResources(parts map[string][]byte, progress editor
return nil, err
}
}
if e.output != nil {
input = imageInput{ReadCloser: input, context: e.output.ctx}
}
resource, safe := refs.scan(input, name)
input.Close()
if e.output != nil && e.output.ctx.Err() != nil {
return nil, e.output.ctx.Err()
}
if !safe {
return nil, nil
}
@@ -224,79 +252,16 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
return false, !strings.EqualFold(path.Ext(name), ".xml")
}
decoder := xml.NewDecoder(buffer)
var stack []string
var fonts []*editorFontUsage
var text strings.Builder
resource, base := false, ""
scan := editorReferenceScan{refs: r, name: name, safe: true}
for {
token, err := decoder.Token()
if err == io.EOF {
return resource, len(stack) == 0
return scan.resource, len(scan.stack) == 0
}
if err != nil {
return false, false
}
switch token := token.(type) {
case xml.StartElement:
if token.Name.Space != "" && token.Name.Space != ofdNamespace {
return false, false
}
if token.Name.Local == "CustomTags" || token.Name.Local == "Extensions" || token.Name.Local == "ExtendData" || token.Name.Local == "Data" {
return false, false
}
if len(stack) == 0 {
resource = token.Name.Local == "Res"
if resource {
for _, attr := range token.Attr {
if attr.Name.Local == "BaseLoc" {
base = attr.Value
}
}
}
}
var usage *editorFontUsage
if len(fonts) != 0 {
usage = fonts[len(fonts)-1]
if usage != nil && (stack[len(stack)-1] == "TextCode" || stack[len(stack)-1] == "Glyphs") {
usage.unsafe = true
}
}
for _, attr := range token.Attr {
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
continue
}
if attr.Name.Space != "" {
return false, false
}
if attr.Name.Local != "ID" {
r.reference(name, base, attr.Name.Local, attr.Value, resource)
}
if attr.Name.Local == "Font" {
usage = r.fontUsage(attr.Value)
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
usage.unsafe = true
}
} else if attr.Name.Local == "Substitution" {
r.fontUsage(attr.Value).unsafe = true
}
}
stack = append(stack, token.Name.Local)
fonts = append(fonts, usage)
text.Reset()
case xml.CharData:
text.Write(token)
case xml.EndElement:
r.reference(name, base, token.Name.Local, text.String(), resource)
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(text.String()) != "" {
r.fontUsage(text.String()).unsafe = true
}
if usage := fonts[len(fonts)-1]; usage != nil {
usage.text(token.Name.Local, text.String())
}
stack = stack[:len(stack)-1]
fonts = fonts[:len(fonts)-1]
text.Reset()
case xml.Directive:
if !scan.accept(token) {
return false, false
}
}
@@ -305,32 +270,44 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
// reference 记录标准资源标识和路径引用,不将普通数字或资源自身ID视为引用
// 入参: name 当前文件, base 资源目录, key 字段, value 值, resource 是否资源索引
func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) {
kind := editorResourceReferenceKind(key, resource)
if kind == editorReferenceNone {
return
}
value = strings.TrimSpace(value)
if value == "" {
return
}
switch key {
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "DefaultCS", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId":
if kind == editorReferenceID {
if id := editorResourceID(value); id != "" {
r.ids[id] = true
}
return
case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "FileRef", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File":
case "BaseLoc":
if resource {
return
}
case "FontFile", "MediaFile":
if resource {
return
}
default:
return
}
r.files[editorResourceLocation(name, base, value)] = true
r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true
}
// editorResourceReferenceKind 区分标准标识和文件引用,其他文本不参与资源扫描
// 入参: key 字段名称, resource 是否资源索引
// 返回: int 引用类型
func editorResourceReferenceKind(key string, resource bool) int {
switch key {
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "DefaultCS", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId":
return editorReferenceID
case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "FileRef", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File":
return editorReferenceFile
case "BaseLoc":
case "FontFile", "MediaFile":
default:
return editorReferenceNone
}
if resource {
return editorReferenceNone
}
return editorReferenceFile
}
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示
// 入参: value 标识文本
// 返回: string 标准十进制标识,无效时为空
+147
View File
@@ -0,0 +1,147 @@
// 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 (
"archive/zip"
"bytes"
"compress/flate"
"encoding/xml"
"hash/crc32"
"io"
"unicode/utf8"
)
// editorStagedPage 保存单次写出的压缩页面及校验值,不持有未压缩XML
type editorStagedPage struct {
data []byte
size uint64
checksum uint32
}
// editorPageStager 复用单次保存的页面压缩器,不跨文档共享状态
type editorPageStager struct {
compressor *flate.Writer
}
// editorPageEncoding 同步统计编码字节及CRC,保留实际写入错误
type editorPageEncoding struct {
writer io.Writer
size uint64
checksum uint32
}
// Write 写入页面字节并统计实际接收的部分
// 入参: data 未压缩编码数据
// 返回: int 实际字节数, error 写入或短写错误
func (w *editorPageEncoding) Write(data []byte) (int, error) {
n, err := w.writer.Write(data)
w.size += uint64(n)
w.checksum = crc32.Update(w.checksum, crc32.IEEETable, data[:n])
if n != len(data) && err == nil {
err = io.ErrShortWrite
}
return n, err
}
// encode 边编码边压缩自产页面,同步收集引用及用字
// 入参: editor 保存快照, page 页面内容, references 本次引用扫描
// 返回: *editorStagedPage 压缩页面, error 编码或取消错误
func (s *editorPageStager) encode(editor *Editor, page PageContent, references *editorReferenceScan) (*editorStagedPage, error) {
var data bytes.Buffer
if s.compressor == nil {
level := flate.DefaultCompression
if editor.output != nil && editor.output.options.Mode != CompressionUnchanged {
level = flate.BestCompression
}
var err error
s.compressor, err = flate.NewWriter(&data, level)
if err != nil {
return nil, err
}
} else {
s.compressor.Reset(&data)
}
output := editorPageEncoding{writer: s.compressor}
x := &ofdXML{encoder: xml.NewEncoder(&output), references: references}
if editor.output != nil {
x.ctx = editor.output.ctx
}
x.page(page)
if err := x.finish(); err != nil {
return nil, err
}
if err := s.compressor.Close(); err != nil {
return nil, err
}
if !x.checkContext() {
return nil, x.err
}
return &editorStagedPage{data: data.Bytes(), size: output.size, checksum: output.checksum}, nil
}
// open 流式读取暂存页面,仅用于无法完整收集编码引用的分支
// 返回: io.ReadCloser 页面解压流
func (p *editorStagedPage) open() io.ReadCloser {
return flate.NewReader(bytes.NewReader(p.data))
}
// writeStagedPage 直接写入已经压缩的自产页面,保持ZIP条目属性和取消语义
// 入参: archive 输出归档, header 条目属性, page 暂存页面
// 返回: error 写入、短写或取消错误
func (e *Editor) writeStagedPage(archive *zip.Writer, header zip.FileHeader, page *editorStagedPage) error {
if e.output != nil && e.output.options.Mode != CompressionUnchanged {
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil {
return err
}
}
header.Method, header.ReaderVersion = zip.Deflate, 20
header.CreatorVersion = header.CreatorVersion&0xff00 | 20
header.CRC32, header.UncompressedSize64, header.CompressedSize64 = page.checksum, page.size, uint64(len(page.data))
header.Flags &^= 8
if header.NonUTF8 {
header.Flags &^= 0x800
} else if utf8.ValidString(header.Name) && utf8.ValidString(header.Comment) {
for _, value := range header.Name + header.Comment {
if value < 0x20 || value > 0x7d || value == 0x5c {
header.Flags |= 0x800
break
}
}
}
entry, err := archive.CreateRaw(&header)
if err != nil {
return err
}
for offset := 0; offset < len(page.data); {
if e.output != nil {
if err := e.output.ctx.Err(); err != nil {
return err
}
}
end := min(offset+64<<10, len(page.data))
if n, err := entry.Write(page.data[offset:end]); err != nil {
return err
} else if n != end-offset {
return io.ErrShortWrite
}
offset = end
}
if e.output != nil && e.output.options.Mode != CompressionUnchanged {
e.output.completed++
return editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0)
}
return nil
}
+25 -2
View File
@@ -133,6 +133,12 @@ type resolvedFontResult struct {
err error
}
// fontDataKey 区分只读字体的起点和长度,指针保留源数据直到预算淘汰
type fontDataKey struct {
data *byte
size int
}
// ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别
// 入参: id 字体资源标识, exact 是否要求精确匹配
// 返回: ResolvedFont 字体与来源, error 解析错误
@@ -165,7 +171,7 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
prepared := &PreparedFont{ResolvedFont: resolved}
definition := r.Reader.fontCache[id]
if definition != nil && definition.FontFile != "" {
key := sha256.Sum256(resolved.Data)
key := r.fontDigest(resolved.Data)
if content, ok := r.fontPreparations.get(key); ok {
prepared.Data, prepared.Glyphs, prepared.CIDs, prepared.digest = content.Data, content.Glyphs, content.CIDs, content.digest
r.preparedFonts[id] = prepared
@@ -188,7 +194,7 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
}
}
}
prepared.digest = sha256.Sum256(prepared.Data)
prepared.digest = r.fontDigest(prepared.Data)
cost := min(len(prepared.Data), r.fontPreparations.limit)*2 + (len(prepared.Glyphs)+len(prepared.CIDs))*48 + 256
r.fontPreparations.put(key, PreparedFont{ResolvedFont: ResolvedFont{Data: prepared.Data}, Glyphs: prepared.Glyphs, CIDs: prepared.CIDs, digest: prepared.digest}, cost)
}
@@ -196,6 +202,22 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) {
return prepared, nil
}
// fontDigest 在预算内复用只读字体摘要,内容副本仍按摘要复用解析结果
// 入参: data 不得修改的字体数据
// 返回: [32]byte 字体摘要
func (r *Renderer) fontDigest(data []byte) [32]byte {
if len(data) == 0 {
return sha256.Sum256(nil)
}
key := fontDataKey{data: &data[0], size: len(data)}
if digest, ok := r.fontDigests.get(key); ok {
return digest
}
digest := sha256.Sum256(data)
r.fontDigests.put(key, digest, min(cap(data), r.fontDigests.limit)+256)
return digest
}
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache()
@@ -206,6 +228,7 @@ func (r *Renderer) resetFontCache() {
func (r *Renderer) resetFontBackendCache() {
r.sharedBackendStates = make(map[any]any)
r.preparedFonts = make(map[string]*PreparedFont)
r.fontDigests = &renderCache[fontDataKey, [32]byte]{limit: 4 << 20}
r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 16 << 20}
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
if r.fontSourcesCache != nil {
+162 -23
View File
@@ -19,6 +19,8 @@ import (
"encoding/binary"
"fmt"
"sort"
"unicode/utf16"
"unicode/utf8"
)
// cmapSegment cmap表段结构
@@ -29,7 +31,7 @@ type cmapSegment struct {
offset uint16
}
// normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号
// normalizeCmap 转换扩展字符子表,保留各编码记录及原字形编号
// 入参: data 字符映射表
// 返回: []byte 等价字符映射表, error 子表错误
func normalizeCmap(data []byte) ([]byte, error) {
@@ -48,7 +50,8 @@ func normalizeCmap(data []byte) ([]byte, error) {
if uint64(offset)+2 > uint64(len(data)) {
return nil, fmt.Errorf("invalid cmap subtable offset")
}
if binary.BigEndian.Uint16(data[offset:]) != 2 {
format := binary.BigEndian.Uint16(data[offset:])
if format != 2 && format != 8 && format != 10 && format != 13 {
continue
}
if out == nil {
@@ -56,17 +59,35 @@ func normalizeCmap(data []byte) ([]byte, error) {
}
next, ok := converted[offset]
if !ok {
mapping, err := parseCmapFormat2(data[offset:])
var mapping map[rune]uint16
var err error
language := uint32(0)
if format == 2 {
mapping, err = parseCmapFormat2(data[offset:])
if err == nil {
language = uint32(binary.BigEndian.Uint16(data[offset+4:]))
}
} else {
mapping, err = parseCmapExtended(data[offset:])
if err == nil {
language = binary.BigEndian.Uint32(data[offset+8:])
}
}
if err != nil {
return nil, err
}
table := buildCmapTable(0, mapping)
if language > 65535 {
table = buildCmapTableFormat12(0, mapping)
}
subtable := table[binary.BigEndian.Uint32(table[8:]):]
language := binary.BigEndian.Uint16(data[offset+4:])
if binary.BigEndian.Uint16(subtable) == 12 {
binary.BigEndian.PutUint32(subtable[8:], uint32(language))
binary.BigEndian.PutUint32(subtable[8:], language)
} else {
binary.BigEndian.PutUint16(subtable[4:], language)
binary.BigEndian.PutUint16(subtable[4:], uint16(language))
}
if uint64(len(out))+uint64(len(subtable)) > 1<<32-1 {
return nil, fmt.Errorf("normalized cmap exceeds offset range")
}
next = uint32(len(out))
converted[offset] = next
@@ -80,6 +101,110 @@ func normalizeCmap(data []byte) ([]byte, error) {
return out, nil
}
// parseCmapExtended 读取混合字符、扩展数组及常量分组,限制到有效Unicode范围
// 入参: data 格式8、10或13子表
// 返回: map[rune]uint16 独立字符映射, error 结构或映射错误
func parseCmapExtended(data []byte) (map[rune]uint16, error) {
if len(data) < 12 {
return nil, fmt.Errorf("truncated extended cmap")
}
length := uint64(binary.BigEndian.Uint32(data[4:]))
if length < 12 || length > uint64(len(data)) {
return nil, fmt.Errorf("invalid extended cmap length")
}
data = data[:int(length)]
format := binary.BigEndian.Uint16(data)
result := make(map[rune]uint16)
add := func(code uint32, glyph uint16) error {
char := rune(code)
if code > utf8.MaxRune {
high, low := rune(code>>16), rune(code&65535)
if format != 8 || high < 0xd800 || high > 0xdbff || low < 0xdc00 || low > 0xdfff {
return fmt.Errorf("invalid extended cmap character")
}
char = utf16.DecodeRune(high, low)
}
if glyph != 0 && utf8.ValidRune(char) {
if previous, exists := result[char]; exists && previous != glyph {
return fmt.Errorf("ambiguous extended cmap character")
}
result[char] = glyph
}
return nil
}
if format == 10 {
if len(data) < 20 {
return nil, fmt.Errorf("truncated cmap format 10")
}
first, count := uint64(binary.BigEndian.Uint32(data[12:])), uint64(binary.BigEndian.Uint32(data[16:]))
if count > uint64((len(data)-20)/2) || first+count > utf8.MaxRune+1 {
return nil, fmt.Errorf("invalid cmap format 10 range")
}
for index := uint64(0); index < count; index++ {
if err := add(uint32(first+index), binary.BigEndian.Uint16(data[20+int(index)*2:])); err != nil {
return nil, err
}
}
return result, nil
}
offset := 16
if format == 8 {
offset = 8208
} else if format != 13 {
return nil, fmt.Errorf("invalid extended cmap format %d", format)
}
if len(data) < offset {
return nil, fmt.Errorf("truncated cmap format %d", format)
}
count := uint64(binary.BigEndian.Uint32(data[offset-4:]))
if count > uint64((len(data)-offset)/12) {
return nil, fmt.Errorf("invalid extended cmap group count")
}
var previous uint32
for index := uint64(0); index < count; index++ {
group := data[offset+int(index)*12:]
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), uint64(binary.BigEndian.Uint32(group[8:]))
if first > last || index != 0 && first <= previous {
return nil, fmt.Errorf("invalid extended cmap group order")
}
previous = last
endGlyph := glyph
if format == 8 {
endGlyph += uint64(last) - uint64(first)
if first <= 65535 && last > 65535 {
return nil, fmt.Errorf("mixed cmap character range")
}
if last > utf8.MaxRune && (first>>16 != last>>16 || first>>16 < 0xd800 || last>>16 > 0xdbff || first&65535 < 0xdc00 || last&65535 > 0xdfff) {
return nil, fmt.Errorf("invalid UTF-16 cmap group")
}
} else if last > utf8.MaxRune {
return nil, fmt.Errorf("invalid cmap Unicode range")
}
if endGlyph > 65535 {
return nil, fmt.Errorf("extended cmap glyph index overflow")
}
for code := uint64(first); code <= uint64(last); code++ {
if format == 8 {
word := code
if code > 65535 {
word >>= 16
}
if (data[12+word/8]&(1<<(7-word%8)) != 0) != (code > 65535) {
return nil, fmt.Errorf("invalid cmap character width")
}
}
id := glyph
if format == 8 {
id += code - uint64(first)
}
if err := add(uint32(code), uint16(id)); err != nil {
return nil, err
}
}
}
return result, nil
}
// parseCmapFormat2 解析混合单字节与双字节字符的子头映射
// 入参: data format 2子表
// 返回: map[rune]uint16 编码值到字形编号, error 子头或字形数组越界
@@ -155,8 +280,8 @@ func parseCmapMappings(data []byte) map[rune]uint16 {
if platformID != 0 && platformID != 3 {
continue
}
offset := int(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > len(data) {
offset := uint64(binary.BigEndian.Uint32(data[pos+4 : pos+8]))
if offset+2 > uint64(len(data)) {
continue
}
switch binary.BigEndian.Uint16(data[offset : offset+2]) {
@@ -168,6 +293,19 @@ func parseCmapMappings(data []byte) map[rune]uint16 {
parseCmapFormat6(data[offset:], result)
case 12:
parseCmapFormat12(data[offset:], result)
case 2, 8, 10, 13:
var mapping map[rune]uint16
var err error
if binary.BigEndian.Uint16(data[offset:]) == 2 {
mapping, err = parseCmapFormat2(data[offset:])
} else {
mapping, err = parseCmapExtended(data[offset:])
}
if err == nil {
for char, glyph := range mapping {
result[char] = glyph
}
}
}
}
if len(result) == 0 {
@@ -263,27 +401,28 @@ func parseCmapFormat12(data []byte, result map[rune]uint16) {
if len(data) < 16 {
return
}
length := int(binary.BigEndian.Uint32(data[4:8]))
if length > len(data) {
length = len(data)
length := uint64(binary.BigEndian.Uint32(data[4:8]))
if length < 16 || length > uint64(len(data)) {
return
}
nGroups := int(binary.BigEndian.Uint32(data[12:16]))
for i := 0; i < nGroups; i++ {
pos := 16 + i*12
if pos+12 > length {
break
}
nGroups := uint64(binary.BigEndian.Uint32(data[12:16]))
if nGroups > (length-16)/12 {
return
}
for i := uint64(0); i < nGroups; i++ {
pos := 16 + int(i)*12
startChar := binary.BigEndian.Uint32(data[pos:])
endChar := binary.BigEndian.Uint32(data[pos+4:])
startGID := binary.BigEndian.Uint32(data[pos+8:])
for c := startChar; c <= endChar; c++ {
gid := startGID + c - startChar
if gid != 0 && gid <= 0xFFFF {
if startChar > endChar || startGID > 65535 || startChar > utf8.MaxRune {
continue
}
last := min(uint64(endChar), uint64(startChar)+65535-uint64(startGID), uint64(utf8.MaxRune))
for c := uint64(startChar); c <= last; c++ {
gid := uint64(startGID) + c - uint64(startChar)
if gid != 0 && utf8.ValidRune(rune(c)) {
result[rune(c)] = uint16(gid)
}
if c == endChar {
break
}
}
}
}
+13 -2
View File
@@ -104,6 +104,13 @@ func (r *Reader) Fonts() ([]Font, error) {
// 入参: id 字体资源标识
// 返回: []byte 字体数据, error 资源缺失或集合解析错误
func (r *Reader) FontData(id string) ([]byte, error) {
return r.fontData(id, false)
}
// fontData 按调用方所有权读取内嵌字体,集合字体始终独立提取
// 入参: id 字体资源标识, readonly 是否允许借用只读资源
// 返回: []byte 字体数据, error 资源或集合错误
func (r *Reader) fontData(id string, readonly bool) ([]byte, error) {
if r.fontCache[id] == nil {
if _, err := r.Fonts(); err != nil {
return nil, err
@@ -113,7 +120,11 @@ func (r *Reader) FontData(id string) ([]byte, error) {
if f == nil || f.FontFile == "" {
return nil, fmt.Errorf("embedded font %q not found", id)
}
data, err := r.ResData(f.FontFile)
read := r.readFile
if readonly {
read = r.readFileView
}
data, err := read(r.ResPath(f.FontFile))
if err != nil {
return nil, err
}
@@ -223,7 +234,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
name := r.ResPath(of.FontFile)
faces, ok := r.fontFaces[name]
if !ok {
data, err := r.readFile(name)
data, err := r.readFileView(name)
if err != nil {
return nil, err
}
+9 -1
View File
@@ -84,6 +84,14 @@ func newFontSourceCache() *fontSourceCache {
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]fontFileCandidate), filesystems: make(map[int][]fontFileCandidate), data: make(map[fontSourceKey]fontSourceData)}
}
// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立
// 返回: *fontSourceCache 独立文档缓存
func (c *fontSourceCache) child() *fontSourceCache {
cache := newFontSourceCache()
cache.directories, cache.filesystems, cache.data = c.directories, c.filesystems, c.data
return cache
}
// readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择
// 入参: source 字体来源, definition 字体定义
// 返回: []byte 字体数据, error 读取或集合错误
@@ -156,7 +164,7 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool)
definition = &Font{ID: id}
}
if definition.FontFile != "" {
data, err := r.Reader.FontData(id)
data, err := r.Reader.fontData(id, true)
if err != nil {
return ResolvedFont{}, err
}
+3
View File
@@ -42,6 +42,9 @@ func (p GeometryPath) Transform(matrix Matrix) (GeometryPath, error) {
return Point{x, y}
}
var result GeometryPath
if len(p) > 0 {
result = make(GeometryPath, 0, len(p))
}
var current, start Point
for _, s := range p {
originalSegment := s
+67 -27
View File
@@ -21,7 +21,6 @@ import (
"math"
"reflect"
"strconv"
"strings"
"github.com/xiaoqidun/pdfgo"
)
@@ -62,6 +61,12 @@ type PDFImportReport struct {
Warnings []pdfgo.Diagnostic
}
// pdfPointBounds 逐点汇总边界,不保留文字或路径的临时坐标数组
type pdfPointBounds struct {
minX, minY, maxX, maxY float64
found bool
}
// pdfImporter 保存单次转换的对象与资源状态
type pdfImporter struct {
ctx context.Context
@@ -77,7 +82,7 @@ type pdfImporter struct {
clipTexts map[*pdfgo.TextClip][]TextObject
clipPaths *renderCache[[32]byte, pdfClipPath]
cmykSpace string
objects []GraphicObject
objects *pdfObjectBuffer
pages map[pdfgo.Reference]*pdfgo.Page
pageIDs map[pdfgo.Reference]string
pageIndexes map[pdfgo.Reference]int
@@ -143,6 +148,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
editor := NewEditor()
editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
importer.report.Warnings = diagnostics
importer.pageIndexes = make(map[pdfgo.Reference]int)
@@ -270,18 +276,21 @@ func ConvertPDF(ctx context.Context, source io.ReaderAt, size int64, output io.W
}
return nil
}
var written int64
_, err = editor.WriteTo(convertWriter{context: ctx, writer: output, count: &written})
_, err = editor.WriteToWithOptions(ctx, output, WriteOptions{})
return report, err
}
// commitObjects 将当前页面待转换对象写入编辑文档
// 返回: error 对象写入错误
func (p *pdfImporter) commitObjects() error {
if len(p.objects) == 0 {
if p.objects.len() == 0 {
return nil
}
if _, err := p.editor.CopyObjectsContext(p.ctx, p.page, p.objects, 0, 0); err != nil {
objects, err := p.objects.data(p.ctx)
if err != nil {
return err
}
if err := p.editor.appendOwnedObjects(p.ctx, p.page, objects); err != nil {
return err
}
p.objects = nil
@@ -313,11 +322,15 @@ func pdfPageMatrix(page *pdfgo.Page) (pdfgo.Matrix, float64, float64) {
// 入参: values 数值列表
// 返回: string 空格分隔的数值
func pdfNumbers(values ...float64) string {
parts := make([]string, len(values))
var buffer [128]byte
data := buffer[:0]
for n, v := range values {
parts[n] = strconv.FormatFloat(v, 'g', -1, 64)
if n != 0 {
data = append(data, ' ')
}
data = strconv.AppendFloat(data, v, 'g', -1, 64)
}
return strings.Join(parts, " ")
return string(data)
}
// pdfBoundary 序列化对象边界
@@ -329,17 +342,31 @@ func pdfBoundary(b Box) string { return pdfNumbers(b.X, b.Y, b.W, b.H) }
// 入参: points 坐标点集合
// 返回: Box 包围框
func pdfBounds(points []pdfgo.Point) Box {
if len(points) == 0 {
var bounds pdfPointBounds
for _, point := range points {
bounds.add(point)
}
return bounds.box()
}
// add 按原顺序累计一个控制点的最小和最大坐标
// 入参: point 坐标点
func (b *pdfPointBounds) add(point pdfgo.Point) {
if !b.found {
b.minX, b.minY, b.maxX, b.maxY = point.X, point.Y, point.X, point.Y
b.found = true
}
b.minX, b.minY = math.Min(b.minX, point.X), math.Min(b.minY, point.Y)
b.maxX, b.maxY = math.Max(b.maxX, point.X), math.Max(b.maxY, point.Y)
}
// box 取得原精度边界,空集合仍返回零值
// 返回: Box 坐标范围
func (b *pdfPointBounds) box() Box {
if !b.found {
return Box{}
}
minX, minY, maxX, maxY := points[0].X, points[0].Y, points[0].X, points[0].Y
for _, p := range points {
minX = math.Min(minX, p.X)
minY = math.Min(minY, p.Y)
maxX = math.Max(maxX, p.X)
maxY = math.Max(maxY, p.Y)
}
return Box{minX, minY, math.Max(maxX-minX, 1e-6), math.Max(maxY-minY, 1e-6)}
return Box{b.minX, b.minY, math.Max(b.maxX-b.minX, 1e-6), math.Max(b.maxY-b.minY, 1e-6)}
}
// pathBoundsContext 逐段计算变换后的控制点边界,不创建临时点集合
@@ -462,12 +489,12 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
if err := p.flushPath(); err != nil {
return nil, err
}
count := len(p.objects)
count := p.objects.len()
textCount := p.report.TextObjects
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
return nil, err
}
p.objects = p.objects[:count]
p.objects.truncate(count)
p.report.TextObjects = textCount
objects = p.clipTexts[mark]
}
@@ -617,7 +644,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
phase := mark.Style.DashPhase * scale
object.DashOffset = &phase
}
p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object})
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
p.report.PathObjects++
return nil
}
@@ -737,14 +764,18 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
if err := cell.flushPath(); err != nil {
return nil, err
}
pattern.CellContent.Objects = cell.objects
objects, err := cell.objects.data(p.ctx)
if err != nil {
return nil, err
}
pattern.CellContent.Objects = objects
if width > xstep || height > ystep {
columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep)
if !finite(columns*rows) || columns*rows > float64(int(^uint(0)>>1)) {
return nil, fmt.Errorf("PDF tiling pattern instance count overflow")
}
bounds := make([]Box, len(cell.objects))
for index, object := range cell.objects {
bounds := make([]Box, len(objects))
for index, object := range objects {
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke {
var err error
bounds[index], err = ParseBox(object.PathObject.Boundary)
@@ -753,14 +784,19 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
}
}
}
pattern.CellContent.Objects = nil
var expanded pdfObjectBuffer
for row := 1 - int(rows); row <= 0; row++ {
for column := 1 - int(columns); column <= 0; column++ {
if err := p.ctx.Err(); err != nil {
return nil, err
}
dx, dy := float64(column)*xstep*unit, float64(row)*ystep*unit
for index, object := range cell.objects {
for index, object := range objects {
if index%pdfObjectChunkLimit == 0 {
if err := p.ctx.Err(); err != nil {
return nil, err
}
}
b := bounds[index]
if b.W > 0 && b.H > 0 && (b.X+dx+b.W < 0 || b.Y+dy+b.H < 0 || b.X+dx > pattern.Width || b.Y+dy > pattern.Height) {
continue
@@ -769,10 +805,14 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
if err != nil {
return nil, err
}
pattern.CellContent.Objects = append(pattern.CellContent.Objects, translated)
expanded.append(translated)
}
}
}
pattern.CellContent.Objects, err = expanded.data(p.ctx)
if err != nil {
return nil, err
}
}
return pattern, nil
}
+5 -1
View File
@@ -98,7 +98,11 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
p.compositeNodes = stamp.compositeNodes
p.transferBackdrop = stamp.transferBackdrop
}
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}}
objects, err := stamp.objects.data(ctx)
if err != nil {
return err
}
converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Remark: remark, Appearance: Appearance{Boundary: pdfBoundary(box), Objects: objects}}
if annotation.Subtype == "Redact" {
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}}
}
+119
View File
@@ -0,0 +1,119 @@
// 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"
// pdfObjectChunkLimit 限制导入暂存片段的容量,不限制页面对象数量
const pdfObjectChunkLimit = 256
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
type pdfObjectBuffer struct {
parts [][]GraphicObject
size int
}
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
// 入参: object 独立导入对象
func (p *pdfImporter) appendObject(object GraphicObject) {
if p.objects == nil {
p.objects = &pdfObjectBuffer{}
}
p.objects.append(object)
}
// append 追加对象,小批量连续存储,超过阈值后不复制前序片段
// 入参: object 独立对象
func (b *pdfObjectBuffer) append(object GraphicObject) {
index := len(b.parts) - 1
if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit {
part := b.parts[0]
next := make([]GraphicObject, len(part), min(pdfObjectChunkLimit, max(1, cap(part)*2)))
copy(next, part)
b.parts[0] = next
}
if index < 0 || len(b.parts[index]) == cap(b.parts[index]) {
capacity := pdfObjectChunkLimit
if index < 0 {
capacity = 1
}
b.parts = append(b.parts, make([]GraphicObject, 0, capacity))
index++
}
b.parts[index] = append(b.parts[index], object)
b.size++
}
// len 获取暂存对象数量,空缓冲返回零
// 返回: int 对象数量
func (b *pdfObjectBuffer) len() int {
if b == nil {
return 0
}
return b.size
}
// truncate 回滚到已有对象位置,清理丢弃对象及片段引用
// 入参: size 保留对象数量
func (b *pdfObjectBuffer) truncate(size int) {
if size < 0 || size > b.len() {
panic("invalid PDF object buffer size")
}
if size == b.len() {
return
}
remaining := size
for index, part := range b.parts {
if remaining <= len(part) {
clear(part[remaining:])
b.parts[index] = part[:remaining]
clear(b.parts[index+1:])
b.parts = b.parts[:index+1]
b.size = size
return
}
remaining -= len(part)
}
}
// data 按绘制顺序整理独立对象数组,取消时保留原片段供重试
// 入参: ctx 取消上下文
// 返回: []GraphicObject 暂存数据, error 取消错误
func (b *pdfObjectBuffer) data(ctx context.Context) ([]GraphicObject, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if b.len() == 0 {
return nil, nil
}
if len(b.parts) == 1 {
return b.parts[0][:b.size:b.size], nil
}
objects := make([]GraphicObject, b.size)
offset := 0
for _, part := range b.parts {
if err := ctx.Err(); err != nil {
return nil, err
}
offset += copy(objects[offset:], part)
}
if err := ctx.Err(); err != nil {
return nil, err
}
b.parts[0] = objects
clear(b.parts[1:])
b.parts = b.parts[:1]
return objects, nil
}
+57 -20
View File
@@ -50,6 +50,7 @@ type pdfCompositor struct {
width, height int
inverse pdfgo.Matrix
cache *pdfCompositeCache
scratch *pdfCompositeScratch
masks map[*pdfgo.SoftMask][]float64
meshes map[pdfMeshKey][]pdfShadingPixel
gradients map[pdfGradientKey][]pdfShadingPixel
@@ -71,6 +72,15 @@ type pdfCompositeGeometry struct {
box Box
}
// pdfCompositeColorSampler 在单次绘制内复用完全相同参数的轴向颜色,不近似函数
type pdfCompositeColorSampler struct {
paint pdfgo.Paint
space *pdfgo.ColorSpace
intent pdfgo.Name
conversion pdfgo.ColorConversion
values map[float64][4]float64
}
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct {
geometry map[pdfCompositeKey]pdfCompositeGeometry
@@ -242,8 +252,17 @@ func (p *pdfImporter) compositeTextBounds(mark pdfgo.TextMark, stroke bool) Box
// 返回: []pdfCompositeNode 原始图元, error 解析错误
func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) {
active := make(map[*pdfgo.Font]map[string]bool)
var seenFonts map[*pdfgo.Font]bool
var externalNames map[string]bool
var embeddedFonts map[*pdfgo.Font]bool
var textMarks []pdfgo.TextMark
storeText := func(mark pdfgo.TextMark) *pdfgo.TextMark {
if len(textMarks) == cap(textMarks) {
textMarks = make([]pdfgo.TextMark, 0, min(64, max(8, cap(textMarks)*2)))
}
textMarks = append(textMarks, mark)
return &textMarks[len(textMarks)-1]
}
var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error)
collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) {
var nodes []pdfCompositeNode
@@ -259,7 +278,11 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
return nil
}
v.Text = func(mark pdfgo.TextMark) error {
if mark.Font.Subtype != "Type3" {
if mark.Font.Subtype != "Type3" && !seenFonts[mark.Font] {
if seenFonts == nil {
seenFonts = make(map[*pdfgo.Font]bool)
}
seenFonts[mark.Font] = true
if len(mark.Font.Program) == 0 {
if externalNames == nil {
externalNames = make(map[string]bool)
@@ -277,7 +300,13 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
embeddedFonts[mark.Font] = true
}
}
var text []pdfCompositeNode
text := &nodes
if mark.Object != nil && mark.Object.Knockout {
if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object {
nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object})
}
text = &nodes[len(nodes)-1].children
}
if mark.Font.Subtype != "Type3" {
if err := p.halftone(mark.Style); err != nil {
return err
@@ -289,10 +318,10 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
for i := range mark.Glyphs {
glyph := mark
glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil
text = append(text, pdfCompositeNode{text: &glyph})
*text = append(*text, pdfCompositeNode{text: storeText(glyph)})
}
} else {
text = append(text, pdfCompositeNode{text: &mark})
*text = append(*text, pdfCompositeNode{text: storeText(mark)})
}
} else {
glyphs := active[mark.Font]
@@ -305,25 +334,17 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
}
glyphs[glyph.Name] = true
children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, mark, index, visitor) })
source := mark
children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, source, index, visitor) })
delete(glyphs, glyph.Name)
if err != nil {
return err
}
part := mark
part.Glyphs, part.Positions = mark.Glyphs[index:index+1], mark.Positions[index:index+1]
text = append(text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part})
*text = append(*text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part})
}
}
if mark.Object != nil && mark.Object.Knockout {
if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object {
nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object})
}
node := &nodes[len(nodes)-1]
node.children = append(node.children, text...)
} else {
nodes = append(nodes, text...)
}
return nil
}
v.Image = func(mark pdfgo.ImageMark) error {
@@ -624,6 +645,9 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
// 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景
// 返回: error 合成或保存错误
func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error {
if err := p.ctx.Err(); err != nil {
return err
}
scene, box, err := p.compileGroup(func(local *pdfImporter) error {
if node.group != nil {
return node.coverage(local, false)
@@ -650,21 +674,28 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
if err != nil {
return err
}
if err := p.ctx.Err(); err != nil {
return err
}
output := image.NewNRGBA(image.Rect(0, 0, w, h))
cache := p.compositingCache()
step := 25.4 / p.rasterDPI
buffer := make([]pdfCompositePixel, min(256, w)*min(256, h))
buffer := make([]pdfCompositePixel, min(pdfCompositeTileSize, w)*min(pdfCompositeTileSize, h))
scratch := &pdfCompositeScratch{}
var transfers []*pdfgo.TransferFunction
var previousTransfers []*pdfgo.TransferFunction
if pdfCompositeHasTransfer(backdrop) || pdfCompositeHasTransfer([]pdfCompositeNode{node}) {
transfers = make([]*pdfgo.TransferFunction, len(buffer))
previousTransfers = make([]*pdfgo.TransferFunction, len(buffer))
}
for y := 0; y < h; y += 256 {
for x := 0; x < w; x += 256 {
for y := 0; y < h; y += pdfCompositeTileSize {
for x := 0; x < w; x += pdfCompositeTileSize {
if err := p.ctx.Err(); err != nil {
return err
}
runtime.Gosched()
width, height := min(256, w-x), min(256, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}}
if transfers != nil {
c.transfers = transfers[:width*height]
clear(c.transfers)
@@ -682,6 +713,11 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
return err
}
for i, pixel := range pixels {
if i%pdfCompositeTileSize == 0 {
if err := p.ctx.Err(); err != nil {
return err
}
}
if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
continue
}
@@ -711,6 +747,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
}
output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))})
}
c.releaseMasks()
}
}
return p.appendRasterGroup(output, box, 1)
+119 -23
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"context"
"fmt"
"math"
@@ -25,6 +26,9 @@ import (
// 入参: nodes 图元, pixels 背景与输出像素, space 混合空间
// 返回: error 几何或颜色错误
func (c *pdfCompositor) draw(nodes []pdfCompositeNode, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
if err := c.importer.ctx.Err(); err != nil {
return err
}
for _, node := range nodes {
if err := c.importer.ctx.Err(); err != nil {
return err
@@ -59,9 +63,13 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
result := pixels
grouped := fill && stroke && (style.FillOverprint || style.StrokeOverprint) && style.Fill.Alpha == style.Stroke.Alpha
if fill && stroke {
initial = append([]pdfCompositePixel(nil), pixels...)
initial = c.acquirePixels(len(pixels))
defer c.releasePixels(initial)
copy(initial, pixels)
if grouped {
result = append([]pdfCompositePixel(nil), pixels...)
result = c.acquirePixels(len(pixels))
defer c.releasePixels(result)
copy(result, pixels)
for i := range result {
result[i].effect = 0
result[i].shape = 0
@@ -88,7 +96,7 @@ func (c *pdfCompositor) drawMark(node pdfCompositeNode, pixels []pdfCompositePix
}
}
if grouped {
return pdfCompositeGroup(pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, space, style.Fill.Alpha, mask, style.BlendMode, style.RenderingIntent, style.ColorConversion)
}
return nil
}
@@ -146,6 +154,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
if err != nil {
return err
}
defer c.releasePixels(pattern)
}
var shading []pdfShadingPixel
if paint.Mesh != nil && (node.image == nil || node.image.Image.ImageMask) {
@@ -161,6 +170,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
}
var uniformValues [4]float64
uniformReady := false
var sampler *pdfCompositeColorSampler
if g := paint.Axial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil &&
(g.Start.Y == g.End.Y && c.inverse[2] == 0 || g.Start.X == g.End.X && c.inverse[1] == 0) {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion}
}
geometry, err := c.geometry(node, outline)
if err != nil {
return err
@@ -231,7 +245,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
opacity *= alpha
} else {
var visible bool
values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion)
if sampler != nil {
values, visible, err = sampler.color(point)
} else {
values, visible, err = pdfCompositeColor(paint, point, space, style.RenderingIntent, style.ColorConversion)
}
if !visible {
shape = 0
}
@@ -311,11 +329,19 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
if !g.Isolated && !g.Knockout && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) && space.Equal(parent) {
return c.draw(node.children, pixels, space)
}
initial := make([]pdfCompositePixel, len(pixels))
if !g.Isolated {
initial := c.acquirePixels(len(pixels))
defer c.releasePixels(initial)
if g.Isolated {
clear(initial)
} else {
sameSpace := space.Equal(parent)
components := parent.Components()
for i, pixel := range pixels {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return err
}
}
values := pixel.values
if !sameSpace {
var err error
@@ -327,7 +353,9 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
}
}
result := append([]pdfCompositePixel(nil), initial...)
result := c.acquirePixels(len(initial))
defer c.releasePixels(result)
copy(result, initial)
if g.Knockout {
if err := c.drawKnockout(node.children, initial, result, space); err != nil {
return err
@@ -343,20 +371,30 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
}
if g.AlphaIsShape {
for i := range result {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return err
}
}
result[i].shape *= g.Alpha
if mask != nil {
result[i].shape *= mask[i]
}
}
}
return pdfCompositeGroup(pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion)
return pdfCompositeGroup(c.importer.ctx, pixels, initial, result, space, parent, g.Alpha, mask, g.BlendMode, g.RenderingIntent, g.ColorConversion)
}
// drawKnockout 将每个对象与组初始背景合成,再按对象形状替换先前对象
// 入参: nodes 组图元, initial 初始背景, pixels 组输出, space 混合空间
// 返回: error 图元或合成错误
func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels []pdfCompositePixel, space *pdfgo.ColorSpace) error {
candidate := make([]pdfCompositePixel, len(initial))
if err := c.importer.ctx.Err(); err != nil {
return err
}
candidate := c.acquirePixels(len(initial))
defer c.releasePixels(candidate)
clear(candidate)
for _, node := range nodes {
var box Box
var err error
@@ -373,6 +411,9 @@ func (c *pdfCompositor) drawKnockout(nodes []pdfCompositeNode, initial, pixels [
continue
}
for y := region.Min.Y; y < region.Max.Y; y++ {
if err := c.importer.ctx.Err(); err != nil {
return err
}
start, end := y*c.width+region.Min.X, y*c.width+region.Max.X
copy(candidate[start:end], initial[start:end])
}
@@ -407,9 +448,12 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit
}
// pdfCompositeGroup 移除组初始背景贡献后,将组结果合成到父空间
// 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 返回: error 颜色或混合错误
func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
func pdfCompositeGroup(ctx context.Context, pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
if err := ctx.Err(); err != nil {
return err
}
if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) {
return fmt.Errorf("invalid PDF transparency group buffers")
}
@@ -420,6 +464,11 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren
}
components, parentComponents := space.Components(), parent.Components()
for i, pixel := range result {
if i%pdfCompositeTileSize == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
alpha := opacity
if mask != nil {
alpha *= mask[i]
@@ -441,6 +490,9 @@ func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, paren
// 入参: mask 蒙版或空值, inherited 未指定蒙版空间时继承的空间
// 返回: []float64 蒙版透明度或空值, error 蒙版错误
func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) ([]float64, error) {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
if mask == nil {
return nil, nil
}
@@ -450,7 +502,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
nodes, ok := c.cache.masks[mask]
var err error
if !ok {
nodes, err = c.importer.collectCompositeNodes(mask.Walk)
nodes, err = c.importer.collectCompositeNodes(func(v pdfgo.Visitor) error { return mask.WalkContext(c.importer.ctx, v) })
if err != nil {
return nil, err
}
@@ -460,9 +512,16 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
if space == nil {
space = inherited
}
pixels := make([]pdfCompositePixel, c.width*c.height)
pixels := c.acquirePixels(c.width * c.height)
defer c.releasePixels(pixels)
clear(pixels)
if mask.Subtype == "Luminosity" {
for i := range pixels {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
}
copy(pixels[i].values[:], mask.Backdrop)
pixels[i].alpha = 1
}
@@ -472,8 +531,19 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
if err := local.draw(nodes, pixels, space); err != nil {
return nil, err
}
result := make([]float64, len(pixels))
result := c.scratch.masks.acquire(len(pixels))
complete := false
defer func() {
if !complete {
c.scratch.masks.release(result)
}
}()
for i, pixel := range pixels {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
}
v := pixel.alpha
if mask.Subtype == "Luminosity" {
v, err = space.Luminosity(pixel.values[:space.Components()], "RelativeColorimetric")
@@ -486,6 +556,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
return nil, err
}
}
complete = true
c.masks[mask] = result
return result, nil
}
@@ -494,15 +565,6 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
// 入参: mark 图像, point 页面坐标, space 混合空间
// 返回: [4]float64 源颜色, float64 图像透明度, error 解码或变换错误
func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, space *pdfgo.ColorSpace) ([4]float64, float64, error) {
source := c.cache.images[mark.Image]
if source == nil {
var err error
source, err = mark.Image.DecodeComponentsContext(c.importer.ctx)
if err != nil {
return [4]float64{}, 0, err
}
c.cache.images[mark.Image] = source
}
inverse, found := c.cache.imageMatrix[mark]
if !found {
var ok bool
@@ -519,6 +581,15 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
if point.X < 0 || point.X > 1 || point.Y < 0 || point.Y > 1 {
return [4]float64{}, 0, nil
}
source := c.cache.images[mark.Image]
if source == nil {
var err error
source, err = mark.Image.DecodeComponentsViewContext(c.importer.ctx)
if err != nil {
return [4]float64{}, 0, err
}
c.cache.images[mark.Image] = source
}
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
_, nativeProcess := source.Process.CMYKMask()
process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0
@@ -724,6 +795,31 @@ func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorS
return values, true, err
}
// color 复用精确参数对应的颜色,区域、背景及其他画刷保持原求值路径
// 入参: point 页面坐标
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, error) {
g := s.paint.Axial
if g == nil || g.Bounds != nil || g.Background != nil || s.paint.Function != nil || s.paint.Tiling != nil {
return pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion)
}
position, visible := pdfShadingPosition(s.paint, point)
if !visible {
return [4]float64{}, false, nil
}
if values, ok := s.values[position]; ok {
return values, true, nil
}
values, visible, err := pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion)
if err == nil && visible && len(s.values) < 2*pdfCompositeTileSize {
if s.values == nil {
s.values = make(map[float64][4]float64)
}
s.values[position] = values
}
return values, visible, err
}
// pdfCompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景, marked 原生过程通道,nil使用普通空间
// 返回: error 未支持的混合模式
+11 -4
View File
@@ -57,11 +57,12 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
return nil, fmt.Errorf("PDF tiling pattern instance range overflow")
}
}
result := make([]pdfCompositePixel, len(backdrop))
result := c.acquirePixels(len(backdrop))
for i, pixel := range backdrop {
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
}
candidate := make([]pdfCompositePixel, len(backdrop))
candidate := c.acquirePixels(len(backdrop))
defer c.releasePixels(candidate)
disjoint := source.BBox.XMax-source.BBox.XMin <= xstep && source.BBox.YMax-source.BBox.YMin <= ystep
pixelStep := 25.4 / c.importer.rasterDPI
localInverse, _ := pattern.local.matrix.Inverse()
@@ -92,10 +93,11 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
}
}
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}}
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}}
if err := cell.draw(pattern.nodes, pixels, space); err != nil {
return nil, err
}
cell.releaseMasks()
for y := range height {
for x := range width {
index := (top+y)*c.width + left + x
@@ -105,6 +107,11 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
}
}
for i := range result {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
}
pixel := &result[i]
if pixel.effect != 0 {
for j := 0; j < space.Components(); j++ {
@@ -187,7 +194,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
source := p.compositingCache().images[node.image.Image]
if source == nil {
var err error
source, err = node.image.Image.DecodeComponentsContext(p.ctx)
source, err = node.image.Image.DecodeComponentsViewContext(p.ctx)
if err != nil {
return false, err
}
+87
View File
@@ -0,0 +1,87 @@
// 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
// pdfCompositeTileSize 限定单次合成块的像素边长
const pdfCompositeTileSize = 256
// pdfCompositeBuffers 在一次局部合成中保留至多八个块容量内的空闲缓冲
type pdfCompositeBuffers[T any] [8][]T
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
type pdfCompositeScratch struct {
pixels pdfCompositeBuffers[pdfCompositePixel]
masks pdfCompositeBuffers[float64]
}
// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
// 入参: count 所需元素数量
// 返回: []T 可由当前调用独占的缓冲
func (s *pdfCompositeBuffers[T]) acquire(count int) []T {
selected := -1
for i, buffer := range s {
if cap(buffer) >= count && (selected < 0 || cap(buffer) < cap(s[selected])) {
selected = i
}
}
if selected < 0 {
return make([]T, count)
}
buffer := s[selected]
s[selected] = nil
return buffer[:count]
}
// release 归还已不再访问的缓冲,超出块容量或空闲槽数量时直接释放
// 入参: buffer 当前调用借用的缓冲
func (s *pdfCompositeBuffers[T]) release(buffer []T) {
if len(buffer) == 0 || cap(buffer) > pdfCompositeTileSize*pdfCompositeTileSize {
return
}
for i, stored := range s {
if stored == nil {
s[i] = buffer
return
}
}
}
// acquirePixels 为当前合成器借用独立像素缓冲
// 入参: count 所需像素数量
// 返回: []pdfCompositePixel 尚未初始化的独占缓冲
func (c *pdfCompositor) acquirePixels(count int) []pdfCompositePixel {
if c.scratch == nil {
c.scratch = &pdfCompositeScratch{}
}
return c.scratch.pixels.acquire(count)
}
// releasePixels 归还当前调用已不再访问的像素缓冲
// 入参: pixels 当前调用借用的缓冲
func (c *pdfCompositor) releasePixels(pixels []pdfCompositePixel) {
if c.scratch != nil {
c.scratch.pixels.release(pixels)
}
}
// releaseMasks 在当前块绘制结束后归还蒙版结果,不复用其他块的坐标缓存
func (c *pdfCompositor) releaseMasks() {
if c.scratch != nil {
for _, mask := range c.masks {
c.scratch.masks.release(mask)
}
}
clear(c.masks)
}
+1 -1
View File
@@ -58,7 +58,7 @@ func (p *pdfImporter) embeddedTextFont(source *pdfgo.Font, glyphs []pdfgo.Glyph)
return nil
}
for _, glyph := range glyphs {
if utf8.RuneCountInString(glyph.Text) != 1 {
if glyph.Name == ".notdef" || utf8.RuneCountInString(glyph.Text) != 1 {
return nil
}
char, _ := utf8.DecodeRuneInString(glyph.Text)
+23 -20
View File
@@ -297,7 +297,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
return err
}
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: objectClips}
p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object})
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
p.report.PathObjects++
}
return nil
@@ -358,6 +358,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...)
editor.fontSourcesCache = p.renderer.fontSourcesCache
if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil {
return nil, box, err
}
@@ -377,7 +378,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if err := local.flushPath(); err != nil {
return nil, box, err
}
if len(local.objects) == 0 {
if local.objects.len() == 0 {
return nil, box, nil
}
if err := local.commitObjects(); err != nil {
@@ -391,17 +392,23 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
page := &editor.pages[0]
renderer := p.renderer.childRenderer(reader)
renderer.TransparentBackground = true
for _, layer := range page.Content.Layer {
for _, object := range layer.Objects {
if err := p.ctx.Err(); err != nil {
return nil, box, err
}
bounds, err := renderer.ObjectBounds(object, layer.DrawParam)
if err != nil {
return nil, box, err
}
box = unionTextBox(box, bounds)
}
scene, err := renderer.CompilePage(page)
if canceled := p.ctx.Err(); canceled != nil {
return nil, box, canceled
}
if err != nil {
return nil, box, err
}
if scene == nil {
return nil, box, fmt.Errorf("PDF local compositing: invalid compiled scene")
}
geometry, err := renderer.Geometry()
if err != nil {
return nil, box, err
}
box, err = scene.BoundsContext(p.ctx, geometry)
if err != nil {
return nil, Box{}, err
}
if box.W <= 0 || box.H <= 0 {
return nil, box, nil
@@ -413,11 +420,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if box.W <= 0 || box.H <= 0 {
return nil, box, nil
}
scene, err := renderer.CompilePage(page)
if canceled := p.ctx.Err(); canceled != nil {
return nil, box, canceled
}
return scene, box, err
return scene, box, nil
}
// renderGroupScene 在指定区域渲染已编译场景,不修改缓存的命令与裁剪
@@ -466,7 +469,7 @@ func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float6
}
alpha := int(math.Round(opacity * 255))
object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha}
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object})
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
p.report.ImageObjects++
if !p.rasterWarned {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)})
@@ -659,7 +662,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) {
region, err = geometry.Combine(region, shape, operation)
return err
}
if err := mask.Walk(visitor); err != nil {
if err := mask.WalkContext(p.ctx, visitor); err != nil {
return pdfgo.Path{}, err
}
path := pdfgo.Path{}
+20 -7
View File
@@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"image"
"image/color"
"math"
"github.com/xiaoqidun/pdfgo"
@@ -36,6 +35,9 @@ func (p *pdfImporter) rasterMaskAllowed(err error) bool {
// 入参: source 源图像, box 页面区域, mask PDF软蒙版
// 返回: image.Image 合成图像, error 蒙版解释或渲染错误
func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.SoftMask) (image.Image, error) {
if err := p.ctx.Err(); err != nil {
return nil, err
}
inverse, ok := p.matrix.Inverse()
if !ok {
return nil, fmt.Errorf("invalid PDF mask transform")
@@ -44,20 +46,31 @@ func (p *pdfImporter) applySoftMask(source image.Image, box Box, mask *pdfgo.Sof
w, h := out.Rect.Dx(), out.Rect.Dy()
step := 25.4 / p.rasterDPI
cache := p.compositingCache()
for y := 0; y < h; y += 256 {
for x := 0; x < w; x += 256 {
width, height := min(256, w-x), min(256, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
alpha, err := c.mask(mask, &pdfgo.ColorSpace{Model: "DeviceRGB"})
scratch := &pdfCompositeScratch{}
space := &pdfgo.ColorSpace{Model: "DeviceRGB"}
for y := 0; y < h; y += pdfCompositeTileSize {
for x := 0; x < w; x += pdfCompositeTileSize {
if err := p.ctx.Err(); err != nil {
return nil, err
}
width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}}
alpha, err := c.mask(mask, space)
if err != nil {
return nil, err
}
for i, opacity := range alpha {
if i%width == 0 {
if err := p.ctx.Err(); err != nil {
return nil, err
}
}
px, py := out.Rect.Min.X+x+i%width, out.Rect.Min.Y+y+i/width
pixel := color.NRGBAModel.Convert(source.At(px, py)).(color.NRGBA)
pixel := imageNRGBAAt(source, px, py)
pixel.A = uint8(math.Round(float64(pixel.A) * opacity))
out.SetNRGBA(px, py, pixel)
}
c.releaseMasks()
}
}
return out, nil
+77 -54
View File
@@ -28,10 +28,11 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// pdfImageKey 区分同一图像流在不同渲染意图下的资源
// pdfImageKey 区分同一图像流的渲染意图及模板填充色
type pdfImageKey struct {
stream *pdfgo.Stream
intent pdfgo.Name
tint color.NRGBA64
}
// pdfImageResource 保存图像的有效颜色空间及对应资源,避免跨资源重映射复用
@@ -84,6 +85,9 @@ func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 {
// 入参: mark PDF图像绘制信息
// 返回: error 错误信息
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)}
}
@@ -111,14 +115,28 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
source := mark.Image
key := pdfImageKey{stream: source.Stream, intent: source.Intent}
id := ""
if !source.ImageMask {
for _, resource := range p.imageIDs[key] {
if reflect.DeepEqual(resource.space, source.ColorSpace) {
id = resource.id
break
if source.ImageMask {
fill := mark.Style.Fill.RGB
if values := mark.Style.Fill.CMYK; values != nil {
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
fill, err = space.RGB(values[:], mark.Style.RenderingIntent)
if err != nil {
return err
}
}
key.tint = color.NRGBA64{
R: uint16(math.Round(fill[0] * 65535)),
G: uint16(math.Round(fill[1] * 65535)),
B: uint16(math.Round(fill[2] * 65535)),
A: 65535,
}
}
id := ""
for _, resource := range p.imageIDs[key] {
if reflect.DeepEqual(resource.space, source.ColorSpace) {
id = resource.id
break
}
}
if id == "" {
jbig2Original, err := source.JBIG2FileContext(p.ctx)
@@ -137,41 +155,27 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
}
if source.ImageMask {
fill := mark.Style.Fill.RGB
if values := mark.Style.Fill.CMYK; values != nil {
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
fill, err = space.RGB(values[:], mark.Style.RenderingIntent)
if err != nil {
return err
}
}
tint := color.NRGBA64{
R: uint16(math.Round(fill[0] * 65535)),
G: uint16(math.Round(fill[1] * 65535)),
B: uint16(math.Round(fill[2] * 65535)),
}
decoded = &pdfStencilImage{source: decoded, fill: tint}
decoded = &pdfStencilImage{source: decoded, fill: key.tint}
}
var encoded bytes.Buffer
if len(jbig2Original) != 0 {
encoded.Write(jbig2Original)
} else if len(jpegOriginal) != 0 {
encoded.Write(jpegOriginal)
} else {
data := jbig2Original
if len(data) == 0 {
data = jpegOriginal
}
if len(data) == 0 {
var encoded bytes.Buffer
if err := pdfgo.EncodePNG(p.ctx, &encoded, decoded); err != nil {
return err
}
data = encoded.Bytes()
}
id, err = p.editor.AddImage(encoded.Bytes())
id, err = p.editor.AddImage(data)
if err != nil {
return err
}
if !source.ImageMask {
if p.imageIDs == nil {
p.imageIDs = map[pdfImageKey][]pdfImageResource{}
}
p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id})
if p.imageIDs == nil {
p.imageIDs = map[pdfImageKey][]pdfImageResource{}
}
p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id})
}
return p.appendImage(mark, id)
}
@@ -188,7 +192,7 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
return err
}
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object})
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
p.report.ImageObjects++
return nil
}
@@ -240,7 +244,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if mark.Clip == nil && p.rasterMaskAllowed(err) {
mask := mark.Style.SoftMask
mark.Style.SoftMask = nil
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Text(mark) })
masked := mark
return p.rasterGroup(pdfgo.GroupMark{Alpha: 1, SoftMask: mask}, func(v pdfgo.Visitor) error { return v.Text(masked) })
}
return err
}
@@ -249,7 +254,8 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
}
if p.warning != nil && ((paintMode == 0 || paintMode == 2) && pdfGradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && pdfGradientError(mark.Style.Stroke) != nil) {
return p.compositeRegion(nil, pdfCompositeNode{text: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
gradient := mark
return p.compositeRegion(nil, pdfCompositeNode{text: &gradient}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
}
font := mark.Font
embedded := len(font.Program) != 0
@@ -311,11 +317,18 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
outline = metrics.(FontOutlines)
}
fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
if stroke && !embedded {
needOutline := stroke
for _, glyph := range mark.Glyphs {
needOutline = needOutline || glyph.Name == ".notdef"
}
if needOutline && !embedded {
if resolved, err := p.editor.editorFont(id); err == nil {
if provider, ok := resolved.(FontOutlines); ok {
metrics, outline = resolved, provider
for _, glyph := range mark.Glyphs {
if glyph.Name == ".notdef" {
continue
}
char, _ := utf8.DecodeRuneInString(glyph.Text)
if utf8.RuneCountInString(glyph.Text) != 1 || resolved.GlyphIndex(char) == 0 && char != 0 {
metrics, outline = nil, nil
@@ -329,7 +342,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
var glyphPaths []pdfgo.Path
const unit = 25.4 / 72
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, 0, 0})
points := make([]pdfgo.Point, 0, 4*len(mark.Glyphs))
var extent pdfPointBounds
object := TextObject{Font: id, Size: mark.Size * unit, HScale: mark.HorizontalScale, TextCode: make([]TextCode, 0, len(mark.Glyphs))}
if embedded {
object.CGTransform = make([]CGTransform, 0, len(mark.Glyphs))
@@ -340,16 +353,16 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
return err
}
gid := glyph.ID
undefined := glyph.Name == ".notdef"
if !embedded && undefined {
gid = 0
}
if embedded && !reused && font.ProgramType == "FontFile" {
gid = imported.type1Glyphs[glyph.Name]
glyph.HasID = true
}
if glyph.Text == "" && glyph.HasID {
character := getUnicodeFromName(glyph.Name)
if character == 0 {
character = packedGlyphRune(gid)
}
glyph.Text = string(character)
if glyph.Text == "" && (glyph.HasID || undefined) {
glyph.Text = "\u00a4"
}
if embedded && !glyph.HasID {
if utf8.RuneCountInString(glyph.Text) != 1 {
@@ -367,18 +380,18 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if !embedded && glyph.Text == "" {
return &pdfgo.UnsupportedError{Feature: "unmapped external font character"}
}
if outlineStroke && !embedded {
if outlineStroke && !embedded && !undefined {
char, _ := utf8.DecodeRuneInString(glyph.Text)
gid = metrics.GlyphIndex(char)
}
origin := mark.Positions[n]
object.TextCode = append(object.TextCode, TextCode{X: pdfNumbers(origin.X * unit), Y: pdfNumbers(-origin.Y * unit), Value: glyph.Text})
if embedded {
count := utf8.RuneCountInString(glyph.Text)
count := utf8.RuneCountInString(glyph.Text)
if embedded || undefined {
object.CGTransform = append(object.CGTransform, CGTransform{CodePosition: position, CodeCount: count, GlyphCount: 1, Glyphs: strconv.Itoa(int(gid))})
position += count
}
if embedded || outlineStroke {
position += count
if embedded || outlineStroke || undefined && outline != nil {
var path GeometryPath
var bounds Box
var err error
@@ -408,7 +421,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
}
}
for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} {
points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y}))
extent.add(m.Apply(pdfgo.Point{X: origin.X*unit + point.X*mark.HorizontalScale, Y: -origin.Y*unit + point.Y}))
}
if outlineStroke {
matrix := mark.Matrix.Mul(pdfgo.Matrix{mark.HorizontalScale / unit, 0, 0, -1 / unit, origin.X, origin.Y})
@@ -420,12 +433,22 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
}
} else {
width := glyph.Width / 1000 * object.Size * mark.HorizontalScale
for _, point := range []pdfgo.Point{{X: 0, Y: -object.Size}, {X: width, Y: -object.Size}, {X: 0, Y: object.Size / 4}, {X: width, Y: object.Size / 4}} {
points = append(points, m.Apply(pdfgo.Point{X: origin.X*unit + point.X, Y: -origin.Y*unit + point.Y}))
bounds := Box{X: 0, Y: -object.Size, W: width, H: object.Size * 1.25}
if undefined {
b := font.BoundingBox
if b == nil {
b = font.FallbackBoundingBox
}
if b != nil {
bounds = Box{X: b.XMin / 1000 * object.Size * mark.HorizontalScale, Y: -b.YMax / 1000 * object.Size, W: (b.XMax - b.XMin) / 1000 * object.Size * mark.HorizontalScale, H: (b.YMax - b.YMin) / 1000 * object.Size}
}
}
for _, point := range []pdfgo.Point{{X: bounds.X, Y: bounds.Y}, {X: bounds.X + bounds.W, Y: bounds.Y}, {X: bounds.X, Y: bounds.Y + bounds.H}, {X: bounds.X + bounds.W, Y: bounds.Y + bounds.H}} {
extent.add(m.Apply(pdfgo.Point{X: origin.X*unit + point.X, Y: -origin.Y*unit + point.Y}))
}
}
}
box := pdfBounds(points)
box := extent.box()
if stroke && !outlineStroke {
strokeMatrix := mark.StrokeMatrix
if strokeMatrix == (pdfgo.Matrix{}) {
@@ -508,7 +531,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
object.Visible = new(bool)
object.Fill, object.Stroke = new(bool), new(bool)
}
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
p.appendObject(GraphicObject{Type: "TextObject", TextObject: object})
p.report.TextObjects++
return nil
}
+1 -12
View File
@@ -68,21 +68,10 @@ func (p *pdfImporter) disjointText(node pdfCompositeNode) (bool, error) {
}
var box Box
for _, command := range scene.Commands {
path := geometryFromRaster(command.Path)
if command.Image != nil {
b := command.Image.Bounds()
path = geometryRectangle(Box{X: float64(b.Min.X), Y: float64(b.Min.Y), W: float64(b.Dx()), H: float64(b.Dy())})
}
path, err = geometry.Transform(path, MatrixFromValues([6]float64(command.Transform)))
path, err := rasterCommandGeometry(geometry, command)
if err != nil {
return false, err
}
if command.Stroke != nil {
path, err = geometry.Stroke(path, *command.Stroke)
if err != nil {
return false, err
}
}
path, err = geometry.Transform(path, MatrixFromValues([6]float64(inverse)))
if err != nil {
return false, err
+5 -2
View File
@@ -102,14 +102,17 @@ type RasterSpread struct {
Period float64
}
// PixelSize 检查页面尺寸并计算目标像素大小
// PixelSize 检查页面尺寸并计算目标像素大小,合法正尺寸至少保留一个像素
// 返回: int 宽度, int 高度, error 无效尺寸
func (p *RasterPage) PixelSize() (int, int, error) {
if p == nil || !rasterPositive(p.Width) || !rasterPositive(p.Height) || !rasterPositive(p.DPI) {
return 0, 0, fmt.Errorf("invalid raster page size or DPI")
}
scale := p.DPI / 25.4
w, h := math.Floor(p.Width*scale+0.5), math.Floor(p.Height*scale+0.5)
if !rasterPositive(scale) {
return 0, 0, fmt.Errorf("invalid raster page size or DPI")
}
w, h := math.Max(1, math.Floor(p.Width*scale+0.5)), math.Max(1, math.Floor(p.Height*scale+0.5))
if !rasterPositive(w) || !rasterPositive(h) || w*h > math.MaxInt32/4 {
return 0, 0, fmt.Errorf("invalid raster pixel dimensions: %g x %g", w, h)
}
+121
View File
@@ -0,0 +1,121 @@
// 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"
)
// Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素
// 包含描边与指令裁剪,不应用页面边界,不排除图片透明像素或渐变透明区域
// 入参: geometry 几何后端
// 返回: Box 毫米坐标范围, error 几何或指令错误
func (p *RasterPage) Bounds(geometry GeometryBackend) (Box, error) {
return p.BoundsContext(context.Background(), geometry)
}
// BoundsContext 按上下文度量只读绘制指令,取消或失败时不返回部分范围
// 入参: ctx 取消上下文, geometry 几何后端
// 返回: Box 毫米坐标范围, error 取消、几何或指令错误
func (p *RasterPage) BoundsContext(ctx context.Context, geometry GeometryBackend) (Box, error) {
if err := ctx.Err(); err != nil {
return Box{}, err
}
if p == nil {
return Box{}, fmt.Errorf("invalid raster page")
}
if geometry == nil {
return Box{}, fmt.Errorf("raster bounds: %w", ErrBackendUnavailable)
}
var box Box
for index, command := range p.Commands {
if err := ctx.Err(); err != nil {
return Box{}, err
}
path, err := rasterCommandGeometry(geometry, command)
if err == nil {
var bounds Box
bounds, err = geometry.Bounds(path)
box = unionTextBox(box, bounds)
}
if err != nil {
return Box{}, fmt.Errorf("raster command %d bounds: %w", index, err)
}
}
if err := ctx.Err(); err != nil {
return Box{}, err
}
return box, nil
}
// rasterCommandGeometry 共用指令的页面变换、描边与裁剪,不修改路径或样式
// 入参: geometry 几何后端, command 绘制指令
// 返回: GeometryPath 页面轮廓, error 几何或指令错误
func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (GeometryPath, error) {
if command.Clip != nil && len(command.Clip) == 0 {
return nil, nil
}
var path GeometryPath
if command.Image != nil {
bounds := command.Image.Bounds()
if bounds.Empty() {
return nil, nil
}
path = geometryRectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())})
} else {
if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 {
return nil, nil
}
for _, segment := range command.Path {
if segment.Verb > RasterClose {
return nil, fmt.Errorf("unsupported raster path verb %d", segment.Verb)
}
}
path = geometryFromRaster(command.Path)
}
matrix := MatrixFromValues([6]float64(command.Transform))
path, err := geometry.Transform(path, matrix)
if err != nil {
return nil, err
}
if command.Image == nil && command.Stroke != nil {
if err := validateStroke(*command.Stroke); err != nil {
return nil, err
}
path, err = geometry.Stroke(path, *command.Stroke)
if err != nil {
return nil, err
}
} else if command.EvenOdd && command.Clip != nil {
path, err = geometry.Normalize(path, true)
if err != nil {
return nil, err
}
}
if command.Clip != nil {
for _, segment := range command.Clip {
if segment.Verb > RasterClose {
return nil, fmt.Errorf("unsupported raster clip verb %d", segment.Verb)
}
}
clip := geometryFromRaster(command.Clip)
path, err = clipGeometry(geometry, path, &clip)
if err != nil {
return nil, err
}
}
return path, nil
}
+24 -4
View File
@@ -524,20 +524,40 @@ func validPackagePath(name string) bool {
// 入参: name 文件名
// 返回: []byte 文件内容, error 错误信息
func (r *Reader) readFile(name string) ([]byte, error) {
data, borrowed, err := r.readFileData(name)
if borrowed {
data = bytes.Clone(data)
}
return data, err
}
// readFileView 借用已加载文件或读取压缩文件,返回数据不得修改
// 入参: name 文件名
// 返回: []byte 只读文件内容, error 错误信息
func (r *Reader) readFileView(name string) ([]byte, error) {
data, _, err := r.readFileData(name)
return data, err
}
// readFileData 统一解析包内路径并区分缓存借用与独立读取
// 入参: name 文件名
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
func (r *Reader) readFileData(name string) ([]byte, bool, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
return data, true, nil
}
if f, ok := r.packageFile(name); ok {
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
return bytes.Clone(data), nil
return data, true, nil
}
return readZipFile(f)
data, err := readZipFile(f)
return data, false, err
}
return nil, fmt.Errorf("file not found: %s", name)
return nil, false, fmt.Errorf("file not found: %s", name)
}
// openFile 打开文档内的文件流
+1 -2
View File
@@ -15,7 +15,6 @@
package ofdgo
import (
"crypto/sha256"
"fmt"
"math"
"strings"
@@ -262,7 +261,7 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
// 返回: FontMetrics 字体度量, error 字体解析错误
func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) {
if prepared.digest == ([32]byte{}) {
prepared.digest = sha256.Sum256(prepared.Data)
prepared.digest = r.fontDigest(prepared.Data)
}
key := prepared.digest
if metrics, ok := r.fontMetrics.get(key); ok {
+4 -1
View File
@@ -49,6 +49,7 @@ type Renderer struct {
sharedBackendStates map[any]any
resolvedFonts map[resolvedFontKey]resolvedFontResult
preparedFonts map[string]*PreparedFont
fontDigests *renderCache[fontDataKey, [32]byte]
fontPreparations *renderCache[[32]byte, PreparedFont]
fontSourcesCache *fontSourceCache
fontMetrics *renderCache[[32]byte, FontMetrics]
@@ -84,7 +85,7 @@ func (r *Renderer) FontSources() ([]string, []fs.FS) {
return append([]string(nil), r.fontDirs...), append([]fs.FS(nil), r.fontFS...)
}
// childRenderer 创建继承当前配置的子渲染器
// childRenderer 继承配置并复用字体文件索引与数据,文档匹配结果保持独立
// 入参: reader 子阅读器
// 返回: *Renderer 子渲染器
func (r *Renderer) childRenderer(reader *Reader) *Renderer {
@@ -102,7 +103,9 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
renderer := NewRenderer(reader, opts...)
renderer.sharedBackendStates = r.sharedBackendStates
renderer.fontPreparations = r.fontPreparations
renderer.fontDigests = r.fontDigests
renderer.fontMetrics = r.fontMetrics
renderer.fontSourcesCache = r.fontSourcesCache.child()
renderer.decodeImages = r.decodeImages
renderer.TransparentBackground = r.TransparentBackground
return renderer
+2 -6
View File
@@ -14,11 +14,7 @@
package ofdgo
import (
"crypto/sha256"
"github.com/tdewolff/canvas"
)
import "github.com/tdewolff/canvas"
// loadFont 加载字体
// 入参: fontID 字体ID
@@ -42,7 +38,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
fontData := resolved.Data
fontStyle := canvasFontStyle(of)
if resolved.digest == ([32]byte{}) {
resolved.digest = sha256.Sum256(fontData)
resolved.digest = r.fontDigest(fontData)
}
key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle}
cache := r.canvasFontFamilies()
+10
View File
@@ -67,6 +67,16 @@ func (CanvasBackend) Bounds(path GeometryPath) (Box, error) {
// 入参: path 页面路径, matrix 页面变换
// 返回: GeometryPath 新路径, error 无效路径错误
func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) {
curves := true
for _, segment := range path {
if segment.Verb == GeometryArc {
curves = false
break
}
}
if curves {
return path.Transform(matrix)
}
p, err := geometryToCanvasPath(&path)
if err != nil {
return nil, err
+1 -1
View File
@@ -282,7 +282,7 @@ func shapedLineWidth(runes []rune, advances []float64, spacing float64) float64
// validateTextGlyphs 校验显式字形编号及原文区间,不允许跨TextCode或重叠映射
// 入参: obj 文字对象, glyphCount 字体字形数量
// 返回: error 无效映射
func validateTextGlyphs(obj TextObject, glyphCount uint16) error {
func validateTextGlyphs(obj TextObject, glyphCount int) error {
if len(obj.CGTransform) == 0 {
return nil
}
+137 -21
View File
@@ -17,6 +17,7 @@ package ofdgo
import (
"archive/zip"
"bytes"
"context"
"encoding/hex"
"encoding/xml"
"fmt"
@@ -43,8 +44,13 @@ type ofdCountingWriter struct {
// ofdXML 使用标准ofd命名空间写出XML
type ofdXML struct {
encoder *xml.Encoder
err error
encoder *xml.Encoder
references *editorReferenceScan
ctx context.Context
attrs ofdAttrs
escaped bytes.Buffer
spaces bytes.Buffer
err error
}
// ofdAttrs OFD节点属性
@@ -316,7 +322,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
}
fonts, images, spaces, definitions := e.usedResources()
writeXML := func(name string, encode func(*ofdXML)) error {
data, err := encodeOFDXML(encode)
data, err := e.encodeXML(encode)
if err != nil {
return err
}
@@ -538,6 +544,9 @@ func (x *ofdXML) page(page PageContent) {
x.end("Area")
x.start("Content", nil)
for _, layer := range page.Content.Layer {
if x.err != nil {
return
}
x.layer(layer)
}
x.end("Content")
@@ -554,6 +563,9 @@ func (x *ofdXML) layer(layer Layer) {
attrs.add("DrawParam", layer.DrawParam)
x.start("Layer", attrs)
for _, object := range layer.Objects {
if x.err != nil {
return
}
x.object(object, false)
}
x.end("Layer")
@@ -566,6 +578,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace)
if len(spaces) != 0 {
x.start("ColorSpaces", nil)
for _, space := range spaces {
if x.err != nil {
return
}
x.start("ColorSpace", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: space.ID}, {Name: xml.Name{Local: "Type"}, Value: space.Type}, {Name: xml.Name{Local: "BitsPerComponent"}, Value: strconv.Itoa(space.BitsPerComponent)}})
x.end("ColorSpace")
}
@@ -574,6 +589,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace)
if len(fonts) != 0 {
x.start("Fonts", nil)
for _, resource := range fonts {
if x.err != nil {
return
}
font := resource.font
var attrs ofdAttrs
attrs.add("ID", font.ID)
@@ -600,6 +618,9 @@ func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace)
if len(images) != 0 {
x.start("MultiMedias", nil)
for _, resource := range images {
if x.err != nil {
return
}
image := resource.image
var attrs ofdAttrs
attrs.add("ID", image.ID)
@@ -625,11 +646,21 @@ func newOFDXML(writer io.Writer) *ofdXML {
// token 写入XML标记并保留首个错误
// 入参: token XML标记
func (x *ofdXML) token(token xml.Token) {
if x.err == nil {
if x.checkContext() {
x.err = x.encoder.EncodeToken(token)
x.referenceToken(token)
}
}
// checkContext 保留首个编码或取消错误
// 返回: bool 是否可以继续编码
func (x *ofdXML) checkContext() bool {
if x.err == nil && x.ctx != nil {
x.err = x.ctx.Err()
}
return x.err == nil
}
// root 写入声明命名空间的根节点
// 入参: name 节点名, attrs 属性
func (x *ofdXML) root(name string, attrs ofdAttrs) {
@@ -660,7 +691,7 @@ func (x *ofdXML) text(name, value string) {
// finish 完成XML输出
// 返回: error 错误信息
func (x *ofdXML) finish() error {
if x.err != nil {
if !x.checkContext() {
return x.err
}
return x.encoder.Close()
@@ -702,8 +733,30 @@ func (a *ofdAttrs) alpha(value *int) {
// 入参: encode 节点写入方法
// 返回: []byte XML数据, error 错误信息
func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) {
return encodeOFDXMLContext(nil, encode)
}
// encodeXML 按本次保存上下文编码,不改变普通快照的输出
// 入参: encode 节点写入方法
// 返回: []byte XML数据, error 编码或取消错误
func (e *Editor) encodeXML(encode func(*ofdXML)) ([]byte, error) {
var ctx context.Context
if e.output != nil {
ctx = e.output.ctx
}
return encodeOFDXMLContext(ctx, encode)
}
// encodeOFDXMLContext 编码文档节点,失败时不返回部分数据
// 入参: ctx 取消上下文,可为nil, encode 节点写入方法
// 返回: []byte XML数据, error 编码或取消错误
func encodeOFDXMLContext(ctx context.Context, encode func(*ofdXML)) ([]byte, error) {
var data bytes.Buffer
x := newOFDXML(&data)
x.ctx = ctx
if !x.checkContext() {
return nil, x.err
}
encode(x)
if err := x.finish(); err != nil {
return nil, err
@@ -714,7 +767,10 @@ func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) {
// object 按OFD规定顺序写出基本对象
// 入参: object 图形对象, root 是否为根节点
func (x *ofdXML) object(object GraphicObject, root bool) {
var attrs ofdAttrs
if !x.checkContext() {
return
}
attrs := x.attrs[:0]
var fill, stroke *FillColor
var actions []Action
if root {
@@ -799,6 +855,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.alpha(obj.Alpha)
actions = obj.Actions
}
x.attrs = attrs
x.start(object.Type, attrs)
x.actions(actions)
if object.Type == "ImageObject" {
@@ -817,24 +874,31 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
x.color("StrokeColor", stroke)
offset, index := 0, 0
for _, code := range object.TextObject.TextCode {
if x.err != nil {
return
}
end := offset + len(textCodeRunes(code.Value))
for index < len(object.TextObject.CGTransform) && object.TextObject.CGTransform[index].CodePosition < end {
transform := object.TextObject.CGTransform[index]
attrs := ofdAttrs{
{Name: xml.Name{Local: "CodePosition"}, Value: strconv.Itoa(transform.CodePosition - offset)},
{Name: xml.Name{Local: "CodeCount"}, Value: strconv.Itoa(transform.CodeCount)},
{Name: xml.Name{Local: "GlyphCount"}, Value: strconv.Itoa(transform.GlyphCount)},
if x.err != nil {
return
}
transform := object.TextObject.CGTransform[index]
attrs := x.attrs[:0]
attrs.add("CodePosition", strconv.Itoa(transform.CodePosition-offset))
attrs.add("CodeCount", strconv.Itoa(transform.CodeCount))
attrs.add("GlyphCount", strconv.Itoa(transform.GlyphCount))
x.attrs = attrs
x.start("CGTransform", attrs)
x.text("Glyphs", transform.Glyphs)
x.end("CGTransform")
index++
}
var attrs ofdAttrs
attrs := x.attrs[:0]
attrs.add("X", code.X)
attrs.add("Y", code.Y)
attrs.add("DeltaX", code.DeltaX)
attrs.add("DeltaY", code.DeltaY)
x.attrs = attrs
x.textCode(code.Value, attrs)
offset = end
}
@@ -845,23 +909,35 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
// clips 写出对象裁剪,保留区域内的路径与文字
// 入参: clips 裁剪集合,nil不输出节点
func (x *ofdXML) clips(clips *Clips) {
if clips == nil {
if clips == nil || !x.checkContext() {
return
}
var attrs ofdAttrs
attrs.flag("TransFlag", clips.TransFlag)
x.start("Clips", attrs)
for _, clip := range clips.Clip {
if x.err != nil {
return
}
x.start("Clip", nil)
for _, area := range clip.Area {
if x.err != nil {
return
}
var attrs ofdAttrs
attrs.add("CTM", area.CTM)
attrs.add("DrawParam", area.DrawParam)
x.start("Area", attrs)
for _, path := range area.Path {
if x.err != nil {
return
}
x.object(GraphicObject{Type: "Path", PathObject: path}, false)
}
for _, text := range area.Text {
if x.err != nil {
return
}
x.object(GraphicObject{Type: "Text", TextObject: text}, false)
}
x.end("Area")
@@ -874,23 +950,41 @@ func (x *ofdXML) clips(clips *Clips) {
// textCode 使用XML字符引用转义空格,避免依赖阅读器保留原始空白
// 入参: value OFD文字内容, attrs 文字定位属性
func (x *ofdXML) textCode(value string, attrs ofdAttrs) {
if x.err != nil {
if !x.checkContext() {
return
}
var escaped bytes.Buffer
if x.err = xml.EscapeText(&escaped, []byte(value)); x.err != nil {
x.escaped.Reset()
if x.err = xml.EscapeText(&x.escaped, []byte(value)); x.err != nil {
return
}
data := x.escaped.Bytes()
if next := bytes.IndexByte(data, ' '); next >= 0 {
x.spaces.Reset()
for replaced := 0; next >= 0; replaced++ {
if replaced&255 == 0 && !x.checkContext() {
return
}
x.spaces.Write(data[:next])
x.spaces.WriteString("&#x20;")
data = data[next+1:]
next = bytes.IndexByte(data, ' ')
}
x.spaces.Write(data)
data = x.spaces.Bytes()
}
content := struct {
Text []byte `xml:",innerxml"`
}{Text: bytes.ReplaceAll(escaped.Bytes(), []byte(" "), []byte("&#x20;"))}
}{Text: data}
x.err = x.encoder.EncodeElement(content, xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs})
x.referenceToken(xml.StartElement{Name: xml.Name{Local: "ofd:TextCode"}, Attr: attrs})
x.referenceToken(xml.CharData(value))
x.referenceToken(xml.EndElement{Name: xml.Name{Local: "ofd:TextCode"}})
}
// color 写出纯色、渐变和图案,保留颜色空间与透明度
// 入参: name 节点名, color 颜色
func (x *ofdXML) color(name string, color *FillColor) {
if color == nil {
if color == nil || !x.checkContext() {
return
}
var attrs ofdAttrs
@@ -940,6 +1034,9 @@ func (x *ofdXML) meshShading(name string, extend, columns int, points []ShdPoint
}
x.start(name, attrs)
for _, point := range points {
if x.err != nil {
return
}
var attrs ofdAttrs
attrs.add("X", ofdNumber(point.X))
attrs.add("Y", ofdNumber(point.Y))
@@ -974,6 +1071,9 @@ func shadingAttrs(mapType string, mapUnit float64, extend, start, end string) of
func (x *ofdXML) shading(name string, attrs ofdAttrs, segments []ShdSegment) {
x.start(name, attrs)
for _, segment := range segments {
if x.err != nil {
return
}
var attrs ofdAttrs
if !segment.positionMissing {
attrs.add("Position", ofdNumber(segment.Position))
@@ -988,15 +1088,16 @@ func (x *ofdXML) shading(name string, attrs ofdAttrs, segments []ShdSegment) {
// element 写出具有标准XML标签的子树,显式声明OFD命名空间
// 入参: name 元素名称, value 元素内容
func (x *ofdXML) element(name string, value any) {
if x.err == nil {
if x.checkContext() {
x.err = x.encoder.EncodeElement(value, xml.StartElement{Name: xml.Name{Space: ofdNamespace, Local: name}})
x.referenceElement(name, value)
}
}
// border 写出图片边框及其绘制颜色
// 入参: border 边框,nil不输出节点
func (x *ofdXML) border(border *ImageBorder) {
if border == nil {
if border == nil || !x.checkContext() {
return
}
var attrs ofdAttrs
@@ -1015,6 +1116,9 @@ func (x *ofdXML) border(border *ImageBorder) {
// pattern 写出图案单元,保留对象绘制顺序
// 入参: pattern 图案
func (x *ofdXML) pattern(pattern *Pattern) {
if !x.checkContext() {
return
}
var attrs ofdAttrs
attrs.number("Width", pattern.Width)
attrs.number("Height", pattern.Height)
@@ -1028,6 +1132,9 @@ func (x *ofdXML) pattern(pattern *Pattern) {
content.add("Thumbnail", pattern.CellContent.Thumbnail)
x.start("CellContent", content)
for object := range pattern.CellContent.objects() {
if x.err != nil {
return
}
x.object(object, false)
}
x.end("CellContent")
@@ -1037,19 +1144,28 @@ func (x *ofdXML) pattern(pattern *Pattern) {
// actions 写出对象动作及精确点击区域,不检查链接可达性
// 入参: actions 动作列表
func (x *ofdXML) actions(actions []Action) {
if len(actions) == 0 {
if len(actions) == 0 || !x.checkContext() {
return
}
x.start("Actions", nil)
for _, action := range actions {
if x.err != nil {
return
}
var attrs ofdAttrs
attrs.add("Event", action.Event)
x.start("Action", attrs)
if action.Region != nil {
x.start("Region", nil)
for _, area := range action.Region.Area {
if x.err != nil {
return
}
x.start("Area", ofdAttrs{{Name: xml.Name{Local: "Start"}, Value: area.Start}})
for _, command := range area.Command {
if x.err != nil {
return
}
var attrs ofdAttrs
for _, pair := range [][2]string{{"Point1", command.Point1}, {"Point2", command.Point2}, {"Point3", command.Point3}, {"EllipseSize", command.EllipseSize}, {"RotationAngle", command.RotationAngle}, {"LargeArc", command.LargeArc}, {"SweepDirection", command.SweepDirection}, {"EndPoint", command.EndPoint}} {
attrs.add(pair[0], pair[1])