mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
5dcd7a4e8d
commit
6c14eef1ee
39 files changed
+1874
-368
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
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)
|
||||
|
||||
+13
-7
@@ -53,6 +53,7 @@ type Editor struct {
|
||||
fontDirs []string
|
||||
fontFS []fs.FS
|
||||
fontRenderer *Renderer
|
||||
fontSourcesCache *fontSourceCache
|
||||
fontMetrics map[string]FontMetrics
|
||||
backends RenderBackends
|
||||
images map[string]image.Point
|
||||
@@ -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,15 +1099,13 @@ 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")
|
||||
glyphCount := 1 << 16
|
||||
if !external {
|
||||
glyphCount = int(sfnt.NumGlyphs())
|
||||
}
|
||||
} else {
|
||||
if err := validateTextGlyphs(*obj, sfnt.NumGlyphs()); err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
+80
-2
@@ -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++
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 错误信息
|
||||
|
||||
+90
-19
@@ -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)
|
||||
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}
|
||||
}
|
||||
}
|
||||
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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+57
-80
@@ -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 {
|
||||
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 标准十进制标识,无效时为空
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
+161
-22
@@ -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,26 +401,27 @@ 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 {
|
||||
result[rune(c)] = uint16(gid)
|
||||
if startChar > endChar || startGID > 65535 || startChar > utf8.MaxRune {
|
||||
continue
|
||||
}
|
||||
if c == endChar {
|
||||
break
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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, ' ')
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
data = strconv.AppendFloat(data, v, 'g', -1, 64)
|
||||
}
|
||||
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
@@ -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"}}}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+118
-22
@@ -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
|
||||
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 未支持的混合模式
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+18
-15
@@ -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
|
||||
scene, err := renderer.CompilePage(page)
|
||||
if canceled := p.ctx.Err(); canceled != nil {
|
||||
return nil, box, canceled
|
||||
}
|
||||
bounds, err := renderer.ObjectBounds(object, layer.DrawParam)
|
||||
if err != nil {
|
||||
return nil, box, err
|
||||
}
|
||||
box = unionTextBox(box, bounds)
|
||||
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
@@ -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
|
||||
|
||||
+67
-44
@@ -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,15 +115,29 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
}
|
||||
source := mark.Image
|
||||
key := pdfImageKey{stream: source.Stream, intent: source.Intent}
|
||||
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 := ""
|
||||
if !source.ImageMask {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -137,42 +155,28 @@ 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
|
||||
decoded = &pdfStencilImage{source: decoded, fill: key.tint}
|
||||
}
|
||||
data := jbig2Original
|
||||
if len(data) == 0 {
|
||||
data = jpegOriginal
|
||||
}
|
||||
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}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
var encoded bytes.Buffer
|
||||
if len(jbig2Original) != 0 {
|
||||
encoded.Write(jbig2Original)
|
||||
} else if len(jpegOriginal) != 0 {
|
||||
encoded.Write(jpegOriginal)
|
||||
} else {
|
||||
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})
|
||||
}
|
||||
}
|
||||
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)
|
||||
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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 打开文档内的文件流
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+135
-19
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
@@ -44,6 +45,11 @@ type ofdCountingWriter struct {
|
||||
// ofdXML 使用标准ofd命名空间写出XML
|
||||
type ofdXML struct {
|
||||
encoder *xml.Encoder
|
||||
references *editorReferenceScan
|
||||
ctx context.Context
|
||||
attrs ofdAttrs
|
||||
escaped bytes.Buffer
|
||||
spaces bytes.Buffer
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -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(" ")
|
||||
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(" "))}
|
||||
}{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])
|
||||
|
||||
Reference in new issue
Block a user