feat(文件归并): 归并同类文件清理辅助代码
build.yaml / build (push) Successful in 51s

This commit is contained in:
xiaoqidun committed 2026-10-14 09:38:41 +08:00
1 parent 961bc52038
commit d07972346b
19 files changed
+835 -1042

No files matched your search

+52
View File
@@ -21,6 +21,7 @@ import (
"image"
"io"
"reflect"
"sync"
)
// SVG输出模式,控制资源内嵌和对象结构
@@ -34,6 +35,12 @@ const (
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
var ErrBackendUnavailable = errors.New("render backend unavailable")
// backendRegistry 保存后端工厂,不共享有状态的后端实例
var backendRegistry = struct {
sync.RWMutex
factories map[string]func() RenderBackends
}{factories: make(map[string]func() RenderBackends)}
// Backend 提供稳定的后端标识,不包含前端文案
type Backend interface {
Name() string
@@ -284,3 +291,48 @@ func immutableBackendValue(value reflect.Value) bool {
}
return true
}
// RegisterRenderBackend 注册后端组合,工厂应返回独立实例,重复标识不会覆盖已有实现
// 入参: name 后端标识, factory 后端工厂
// 返回: error 无效或重复注册错误
func RegisterRenderBackend(name string, factory func() RenderBackends) error {
if name == "" || factory == nil {
return fmt.Errorf("backend name and factory are required")
}
backendRegistry.Lock()
defer backendRegistry.Unlock()
if _, exists := backendRegistry.factories[name]; exists {
return fmt.Errorf("render backend %q already registered", name)
}
backendRegistry.factories[name] = factory
return nil
}
// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者
// 入参: name 已注册后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) {
backendRegistry.RLock()
factory := backendRegistry.factories[name]
backendRegistry.RUnlock()
if factory == nil {
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
}
return factory(), nil
}
// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo {
backendRegistry.RLock()
factories := make(map[string]func() RenderBackends, len(backendRegistry.factories))
for name, factory := range backendRegistry.factories {
factories[name] = factory
}
backendRegistry.RUnlock()
result := make(map[string]BackendInfo, len(factories))
for name, factory := range factories {
result[name] = factory().Info()
}
return result
}
-71
View File
@@ -1,71 +0,0 @@
// 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 (
"fmt"
"sync"
)
// backendRegistry 保存后端工厂,不共享有状态的后端实例
var backendRegistry = struct {
sync.RWMutex
factories map[string]func() RenderBackends
}{factories: make(map[string]func() RenderBackends)}
// RegisterRenderBackend 注册后端组合,工厂应返回独立实例,重复标识不会覆盖已有实现
// 入参: name 后端标识, factory 后端工厂
// 返回: error 无效或重复注册错误
func RegisterRenderBackend(name string, factory func() RenderBackends) error {
if name == "" || factory == nil {
return fmt.Errorf("backend name and factory are required")
}
backendRegistry.Lock()
defer backendRegistry.Unlock()
if _, exists := backendRegistry.factories[name]; exists {
return fmt.Errorf("render backend %q already registered", name)
}
backendRegistry.factories[name] = factory
return nil
}
// NewRenderBackends 创建已注册后端组合,各项能力通过Info报告实际提供者
// 入参: name 已注册后端标识
// 返回: RenderBackends 后端组合, error 未知后端错误
func NewRenderBackends(name string) (RenderBackends, error) {
backendRegistry.RLock()
factory := backendRegistry.factories[name]
backendRegistry.RUnlock()
if factory == nil {
return RenderBackends{}, fmt.Errorf("unknown render backend %q", name)
}
return factory(), nil
}
// RenderBackendInfos 列出已注册组合及其实际能力,供原生程序和WASM共用
// 返回: map[string]BackendInfo 后端能力
func RenderBackendInfos() map[string]BackendInfo {
backendRegistry.RLock()
factories := make(map[string]func() RenderBackends, len(backendRegistry.factories))
for name, factory := range backendRegistry.factories {
factories[name] = factory
}
backendRegistry.RUnlock()
result := make(map[string]BackendInfo, len(factories))
for name, factory := range factories {
result[name] = factory().Info()
}
return result
}
+122
View File
@@ -21,6 +21,9 @@ import (
"math"
)
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
const compressionPatternImageLimit = 65536
// compressionImages 记录图片各处使用的最大像素需求
type compressionImages struct {
ctx context.Context
@@ -32,6 +35,12 @@ type compressionImages struct {
patternVisits map[compressionPatternKey]bool
}
// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求
type compressionPatternKey struct {
pattern *Pattern
linear [4]float64
}
// compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理
// 入参: ctx 取消上下文, options 压缩配置
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
@@ -275,3 +284,116 @@ func (v *compressionImages) IncludeHidden() bool { return true }
// SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓
// 返回: bool 始终为真
func (v *compressionImages) SkipClips() bool { return true }
// objectPatterns 统计文字和路径的填充及描边底纹,保留绘制参数继承关系
// 入参: object 对象样式及坐标, state 继承状态
// 返回: error 底纹分析错误
func (v *compressionImages) objectPatterns(object PathObject, state RenderState) error {
fill, stroke := object.FillColor, (*FillColor)(object.StrokeColor)
if defaults := v.renderer.drawParamDefaults(object.DrawParam, state.Defaults); defaults != nil {
if fill == nil {
fill = defaults.FillColor
}
if stroke == nil {
stroke = (*FillColor)(defaults.StrokeColor)
}
}
if (fill == nil || fill.Pattern == nil) && (stroke == nil || stroke.Pattern == nil) {
return nil
}
matrix, _ := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
for _, color := range [2]*FillColor{fill, stroke} {
if color != nil && color.Pattern != nil {
if err := v.patternImages(color, matrix); err != nil {
return err
}
}
}
return nil
}
// patternImages 按独立单元坐标汇总底纹图片及缩略图,不展开重复铺排的单元
// 入参: color 底纹颜色, object 对象到页面矩阵
// 返回: error 取消、循环引用或变换错误
func (v *compressionImages) patternImages(color *FillColor, object Matrix) error {
if err := v.ctx.Err(); err != nil {
return err
}
pattern := color.Pattern
if v.patterns[pattern] {
return fmt.Errorf("cyclic pattern image reference")
}
if !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 {
return fmt.Errorf("invalid pattern cell size")
}
matrix := NewMatrix(pattern.CTM)
switch pattern.RelativeTo {
case "", "Object":
matrix = object.Multiply(matrix)
case "Page":
if v.patternPage != nil {
matrix = v.patternPage.Multiply(matrix)
}
default:
return fmt.Errorf("invalid pattern coordinate system")
}
if !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) {
return fmt.Errorf("invalid pattern transform")
}
matrix.e, matrix.f = 0, 0
key := compressionPatternKey{pattern: pattern, linear: [4]float64{matrix.a, matrix.b, matrix.c, matrix.d}}
if v.patternVisits[key] {
return nil
}
if len(v.patternVisits) >= compressionPatternImageLimit {
return fmt.Errorf("pattern image analysis limit exceeded")
}
if v.patterns == nil {
v.patterns = make(map[*Pattern]bool)
v.patternVisits = make(map[compressionPatternKey]bool)
}
v.patternVisits[key] = true
v.patterns[pattern] = true
defer delete(v.patterns, pattern)
if pattern.CellContent.Thumbnail != "" {
id := editorResourceID(pattern.CellContent.Thumbnail)
if id == "" {
return fmt.Errorf("invalid pattern thumbnail resource")
}
size := compressionImageSize(pattern.Width*math.Hypot(matrix.a, matrix.b), pattern.Height*math.Hypot(matrix.c, matrix.d), v.dpi)
if err := v.recordSize([2]string{id}, size); err != nil {
return err
}
}
columns, rows := 1, 1
switch pattern.ReflectMethod {
case "", "Normal":
case "Column":
columns = 2
case "Row":
rows = 2
case "RowAndColumn":
columns, rows = 2, 2
default:
return fmt.Errorf("invalid pattern reflection")
}
parent := v.patternPage
defer func() { v.patternPage = parent }()
solid := FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace}
defaults := &DrawParam{FillColor: &solid, StrokeColor: (*StrokeColor)(&solid)}
for column := range columns {
for row := range rows {
cell := matrix.Multiply(Matrix{a: float64(1 - 2*column), d: float64(1 - 2*row)})
v.patternPage = &cell
for member := range pattern.CellContent.objects() {
if err := v.ctx.Err(); err != nil {
return err
}
if err := v.renderer.WalkObject(&member, RenderState{Defaults: defaults, Parent: &cell, BoundaryInCTM: true}, v); err != nil {
return err
}
}
}
}
return nil
}
-142
View File
@@ -1,142 +0,0 @@
// 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 (
"fmt"
"math"
)
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
const compressionPatternImageLimit = 65536
// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求
type compressionPatternKey struct {
pattern *Pattern
linear [4]float64
}
// objectPatterns 统计文字和路径的填充及描边底纹,保留绘制参数继承关系
// 入参: object 对象样式及坐标, state 继承状态
// 返回: error 底纹分析错误
func (v *compressionImages) objectPatterns(object PathObject, state RenderState) error {
fill, stroke := object.FillColor, (*FillColor)(object.StrokeColor)
if defaults := v.renderer.drawParamDefaults(object.DrawParam, state.Defaults); defaults != nil {
if fill == nil {
fill = defaults.FillColor
}
if stroke == nil {
stroke = (*FillColor)(defaults.StrokeColor)
}
}
if (fill == nil || fill.Pattern == nil) && (stroke == nil || stroke.Pattern == nil) {
return nil
}
matrix, _ := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
for _, color := range [2]*FillColor{fill, stroke} {
if color != nil && color.Pattern != nil {
if err := v.patternImages(color, matrix); err != nil {
return err
}
}
}
return nil
}
// patternImages 按独立单元坐标汇总底纹图片及缩略图,不展开重复铺排的单元
// 入参: color 底纹颜色, object 对象到页面矩阵
// 返回: error 取消、循环引用或变换错误
func (v *compressionImages) patternImages(color *FillColor, object Matrix) error {
if err := v.ctx.Err(); err != nil {
return err
}
pattern := color.Pattern
if v.patterns[pattern] {
return fmt.Errorf("cyclic pattern image reference")
}
if !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 {
return fmt.Errorf("invalid pattern cell size")
}
matrix := NewMatrix(pattern.CTM)
switch pattern.RelativeTo {
case "", "Object":
matrix = object.Multiply(matrix)
case "Page":
if v.patternPage != nil {
matrix = v.patternPage.Multiply(matrix)
}
default:
return fmt.Errorf("invalid pattern coordinate system")
}
if !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) {
return fmt.Errorf("invalid pattern transform")
}
matrix.e, matrix.f = 0, 0
key := compressionPatternKey{pattern: pattern, linear: [4]float64{matrix.a, matrix.b, matrix.c, matrix.d}}
if v.patternVisits[key] {
return nil
}
if len(v.patternVisits) >= compressionPatternImageLimit {
return fmt.Errorf("pattern image analysis limit exceeded")
}
if v.patterns == nil {
v.patterns = make(map[*Pattern]bool)
v.patternVisits = make(map[compressionPatternKey]bool)
}
v.patternVisits[key] = true
v.patterns[pattern] = true
defer delete(v.patterns, pattern)
if pattern.CellContent.Thumbnail != "" {
id := editorResourceID(pattern.CellContent.Thumbnail)
if id == "" {
return fmt.Errorf("invalid pattern thumbnail resource")
}
size := compressionImageSize(pattern.Width*math.Hypot(matrix.a, matrix.b), pattern.Height*math.Hypot(matrix.c, matrix.d), v.dpi)
if err := v.recordSize([2]string{id}, size); err != nil {
return err
}
}
columns, rows := 1, 1
switch pattern.ReflectMethod {
case "", "Normal":
case "Column":
columns = 2
case "Row":
rows = 2
case "RowAndColumn":
columns, rows = 2, 2
default:
return fmt.Errorf("invalid pattern reflection")
}
parent := v.patternPage
defer func() { v.patternPage = parent }()
solid := FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace}
defaults := &DrawParam{FillColor: &solid, StrokeColor: (*StrokeColor)(&solid)}
for column := range columns {
for row := range rows {
cell := matrix.Multiply(Matrix{a: float64(1 - 2*column), d: float64(1 - 2*row)})
v.patternPage = &cell
for member := range pattern.CellContent.objects() {
if err := v.ctx.Err(); err != nil {
return err
}
if err := v.renderer.WalkObject(&member, RenderState{Defaults: defaults, Parent: &cell, BoundaryInCTM: true}, v); err != nil {
return err
}
}
}
}
return nil
}
-108
View File
@@ -1,108 +0,0 @@
// 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 (
"bufio"
"bytes"
"context"
"encoding/xml"
"io"
)
// editorResourceElement 返回标准资源集合中的声明元素名称
// 入参: group 资源集合名称
// 返回: string 声明元素名称,未知集合返回空值
func editorResourceElement(group string) string {
switch group {
case "Fonts":
return "Font"
case "MultiMedias":
return "MultiMedia"
case "ColorSpaces":
return "ColorSpace"
case "DrawParams":
return "DrawParam"
case "CompositeGraphicUnits":
return "CompositeGraphicUnit"
}
return ""
}
// editorResourceDefinitions 单次流式读取资源声明依赖,保留历史索引和非图像媒体
// 入参: ctx 取消上下文, data 资源索引内容, name 文件路径, source 扫描结果, preserved 是否保留全部声明
// 返回: *editorResourceRefs 含声明依赖的引用,无法安全分析时为空, error 解析或取消错误
func editorResourceDefinitions(ctx context.Context, data []byte, name string, source *editorResourceRefs, preserved bool) (*editorResourceRefs, error) {
result := *source
result.ids, result.fonts = make(map[string]bool), nil
result.definitions = make(map[string]*editorResourceRefs)
outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true}
input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if found, err := editorXMLPrefix(input); err != nil || !found {
return nil, err
}
decoder := xml.NewDecoder(input)
for {
if err := ctx.Err(); err != nil {
return nil, err
}
token, err := decoder.Token()
if err == io.EOF {
if !scan.seen || len(scan.stack) != 0 {
return nil, nil
}
return &result, ctx.Err()
}
if err != nil {
return nil, err
}
depth := len(scan.stack)
if node, ok := token.(xml.StartElement); ok {
if depth == 0 && node.Name.Local != "Res" || depth == 1 && editorResourceElement(node.Name.Local) == "" {
return nil, nil
}
if depth == 2 {
kind := editorResourceElement(scan.stack[1])
var id, mediaType string
for _, attr := range node.Attr {
if attr.Name.Space == "" {
switch attr.Name.Local {
case "ID":
id = editorResourceID(attr.Value)
case "Type":
mediaType = attr.Value
}
}
}
if node.Name.Local != kind || id == "" || result.definitions[id] != nil {
return nil, nil
}
refs := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
result.definitions[id] = refs
scan.refs = refs
if preserved || kind == "MultiMedia" && mediaType != "Image" {
result.ids[id] = true
}
}
}
if !scan.accept(token) {
return nil, ctx.Err()
}
if _, end := token.(xml.EndElement); end && depth == 3 {
scan.refs = outer
}
}
}
+203
View File
@@ -15,14 +15,22 @@
package ofdgo
import (
"bufio"
"bytes"
"context"
"encoding/xml"
"io"
"maps"
"slices"
"strings"
)
// editorResourceScope 记录资源索引的文档归属链,引用集合只读
type editorResourceScope struct {
parent int
refs *editorResourceRefs
}
// scanEditorResources 优先扫描结构依赖,原始数据不按XML解析,孤立文件仍保留引用检查
// 入参: ctx 取消上下文, reader 原包, parts 输出改动, generated 自产引用, names 扫描范围, progress 扫描进度, collect 是否保留各文件的资源引用
// 返回: *editorResourceRefs 全部引用, []string 资源索引, map[string]*editorResourceRefs 文件引用, bool 是否可安全优化, error 读取或取消错误
@@ -147,3 +155,198 @@ func scanEditorResources(ctx context.Context, reader *Reader, parts map[string][
}
return refs, resources, references, true, ctx.Err()
}
// editorResourceElement 返回标准资源集合中的声明元素名称
// 入参: group 资源集合名称
// 返回: string 声明元素名称,未知集合返回空值
func editorResourceElement(group string) string {
switch group {
case "Fonts":
return "Font"
case "MultiMedias":
return "MultiMedia"
case "ColorSpaces":
return "ColorSpace"
case "DrawParams":
return "DrawParam"
case "CompositeGraphicUnits":
return "CompositeGraphicUnit"
}
return ""
}
// editorResourceDefinitions 单次流式读取资源声明依赖,保留历史索引和非图像媒体
// 入参: ctx 取消上下文, data 资源索引内容, name 文件路径, source 扫描结果, preserved 是否保留全部声明
// 返回: *editorResourceRefs 含声明依赖的引用,无法安全分析时为空, error 解析或取消错误
func editorResourceDefinitions(ctx context.Context, data []byte, name string, source *editorResourceRefs, preserved bool) (*editorResourceRefs, error) {
result := *source
result.ids, result.fonts = make(map[string]bool), nil
result.definitions = make(map[string]*editorResourceRefs)
outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true}
input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if found, err := editorXMLPrefix(input); err != nil || !found {
return nil, err
}
decoder := xml.NewDecoder(input)
for {
if err := ctx.Err(); err != nil {
return nil, err
}
token, err := decoder.Token()
if err == io.EOF {
if !scan.seen || len(scan.stack) != 0 {
return nil, nil
}
return &result, ctx.Err()
}
if err != nil {
return nil, err
}
depth := len(scan.stack)
if node, ok := token.(xml.StartElement); ok {
if depth == 0 && node.Name.Local != "Res" || depth == 1 && editorResourceElement(node.Name.Local) == "" {
return nil, nil
}
if depth == 2 {
kind := editorResourceElement(scan.stack[1])
var id, mediaType string
for _, attr := range node.Attr {
if attr.Name.Space == "" {
switch attr.Name.Local {
case "ID":
id = editorResourceID(attr.Value)
case "Type":
mediaType = attr.Value
}
}
}
if node.Name.Local != kind || id == "" || result.definitions[id] != nil {
return nil, nil
}
refs := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
result.definitions[id] = refs
scan.refs = refs
if preserved || kind == "MultiMedia" && mediaType != "Image" {
result.ids[id] = true
}
}
}
if !scan.accept(token) {
return nil, ctx.Err()
}
if _, end := token.(xml.EndElement); end && depth == 3 {
scan.refs = outer
}
}
}
// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果
// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档
// 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误
func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) {
indexes := make(map[string]bool, len(resources))
for _, name := range resources {
indexes[name] = true
}
used := make(map[string]bool)
owners := make(map[string]int)
groups := []editorResourceScope{{}}
groupIDs := make(map[editorResourceScope]int)
for _, entry := range slices.Sorted(maps.Keys(documents)) {
scope := map[string]bool{entry: true}
queue := []string{entry}
refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
definitions := make(map[string]*editorResourceRefs)
for position := 0; position < len(queue); position++ {
if err := ctx.Err(); err != nil {
return nil, err
}
name := queue[position]
source := references[name]
if source == nil {
return nil, ctx.Err()
}
used[name] = true
if selected == "" || selected == entry {
refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts})
for id, definition := range source.definitions {
if definitions[id] != nil {
return nil, nil
}
definitions[id] = definition
}
}
for target, role := range source.roles {
if role&packageReferenceXML != 0 && !scope[target] {
scope[target] = true
queue = append(queue, target)
}
}
}
if selected != "" && selected != entry {
continue
}
var pending []string
if len(definitions) != 0 {
pending = slices.Collect(maps.Keys(refs.ids))
}
for position := 0; position < len(pending); position++ {
if err := ctx.Err(); err != nil {
return nil, err
}
if definition := definitions[pending[position]]; definition != nil {
for id := range definition.ids {
if !refs.ids[id] {
pending = append(pending, id)
}
}
refs.merge(&editorResourceRefs{ids: definition.ids, fonts: definition.fonts})
}
}
for name := range scope {
if err := ctx.Err(); err != nil {
return nil, err
}
if !indexes[name] {
continue
}
key := editorResourceScope{parent: owners[name], refs: refs}
id := groupIDs[key]
if id == 0 {
id = len(groups)
groups = append(groups, key)
groupIDs[key] = id
}
owners[name] = id
}
}
for name, refs := range references {
if !used[name] && (len(refs.ids) != 0 || len(refs.fonts) != 0 || indexes[name]) {
return nil, nil
}
}
result := make(map[string]*editorResourceRefs, len(owners))
merged := make(map[int]*editorResourceRefs)
for name, id := range owners {
if err := ctx.Err(); err != nil {
return nil, err
}
refs := merged[id]
if refs == nil {
refs = groups[id].refs
if groups[id].parent != 0 {
refs = &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
for next := id; next != 0; next = groups[next].parent {
if err := ctx.Err(); err != nil {
return nil, err
}
refs.merge(groups[next].refs)
}
}
merged[id] = refs
}
result[name] = refs
}
return result, ctx.Err()
}
-137
View File
@@ -1,137 +0,0 @@
// 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"
"maps"
"slices"
)
// editorResourceScope 记录资源索引的文档归属链,引用集合只读
type editorResourceScope struct {
parent int
refs *editorResourceRefs
}
// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果
// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档
// 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误
func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) {
indexes := make(map[string]bool, len(resources))
for _, name := range resources {
indexes[name] = true
}
used := make(map[string]bool)
owners := make(map[string]int)
groups := []editorResourceScope{{}}
groupIDs := make(map[editorResourceScope]int)
for _, entry := range slices.Sorted(maps.Keys(documents)) {
scope := map[string]bool{entry: true}
queue := []string{entry}
refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
definitions := make(map[string]*editorResourceRefs)
for position := 0; position < len(queue); position++ {
if err := ctx.Err(); err != nil {
return nil, err
}
name := queue[position]
source := references[name]
if source == nil {
return nil, ctx.Err()
}
used[name] = true
if selected == "" || selected == entry {
refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts})
for id, definition := range source.definitions {
if definitions[id] != nil {
return nil, nil
}
definitions[id] = definition
}
}
for target, role := range source.roles {
if role&packageReferenceXML != 0 && !scope[target] {
scope[target] = true
queue = append(queue, target)
}
}
}
if selected != "" && selected != entry {
continue
}
var pending []string
if len(definitions) != 0 {
pending = slices.Collect(maps.Keys(refs.ids))
}
for position := 0; position < len(pending); position++ {
if err := ctx.Err(); err != nil {
return nil, err
}
if definition := definitions[pending[position]]; definition != nil {
for id := range definition.ids {
if !refs.ids[id] {
pending = append(pending, id)
}
}
refs.merge(&editorResourceRefs{ids: definition.ids, fonts: definition.fonts})
}
}
for name := range scope {
if err := ctx.Err(); err != nil {
return nil, err
}
if !indexes[name] {
continue
}
key := editorResourceScope{parent: owners[name], refs: refs}
id := groupIDs[key]
if id == 0 {
id = len(groups)
groups = append(groups, key)
groupIDs[key] = id
}
owners[name] = id
}
}
for name, refs := range references {
if !used[name] && (len(refs.ids) != 0 || len(refs.fonts) != 0 || indexes[name]) {
return nil, nil
}
}
result := make(map[string]*editorResourceRefs, len(owners))
merged := make(map[int]*editorResourceRefs)
for name, id := range owners {
if err := ctx.Err(); err != nil {
return nil, err
}
refs := merged[id]
if refs == nil {
refs = groups[id].refs
if groups[id].parent != 0 {
refs = &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
for next := id; next != 0; next = groups[next].parent {
if err := ctx.Err(); err != nil {
return nil, err
}
refs.merge(groups[next].refs)
}
}
merged[id] = refs
}
result[name] = refs
}
return result, ctx.Err()
}
+62
View File
@@ -20,6 +20,15 @@ import (
"slices"
)
// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源
// 来源修改、删除或撤销不改变快照,不用于跨文档传输
type ObjectSelection struct {
editor *Editor
document editorDocumentIdentity
objects []GraphicObject
annotations *AnnotationSelection
}
// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存
// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复
// 返回: Box 页面毫米范围, error 标识或度量错误
@@ -358,3 +367,56 @@ func (e *Editor) transformSelection(page int, objects, annotations []string, mat
return nil
})
}
// CaptureSelection 捕获正文与注解,不修改文档或历史
// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复
// 返回: *ObjectSelection 独立快照, error 标识或资源错误
func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) {
items, err := e.Objects(page, objects)
if err != nil {
return nil, err
}
selection := &ObjectSelection{editor: e, document: e.documentIdentity(), objects: items}
if len(annotations) != 0 {
selection.annotations, err = e.CaptureAnnotations(page, annotations)
if err != nil {
return nil, err
}
}
return selection, nil
}
// PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部
// 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
// 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误
func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) {
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) {
return nil, nil, fmt.Errorf("invalid object selection or offset")
}
var objects, annotations []string
err := e.Transaction(func(edit *Editor) error {
var err error
objects, err = edit.CopyObjects(page, selection.objects, dx, dy)
if err == nil && selection.annotations != nil {
annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy)
}
return err
})
if err != nil {
return nil, nil, err
}
return objects, annotations, nil
}
// PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解
// 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
// 返回: []int 新成员序号, error 选区、资源或编辑错误
func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) {
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) {
return nil, fmt.Errorf("object selection belongs to another document or editor")
}
if selection.annotations != nil {
return nil, fmt.Errorf("annotations cannot be nested in a page object")
}
return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy)
}
-79
View File
@@ -1,79 +0,0 @@
// 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 "fmt"
// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源
// 来源修改、删除或撤销不改变快照,不用于跨文档传输
type ObjectSelection struct {
editor *Editor
document editorDocumentIdentity
objects []GraphicObject
annotations *AnnotationSelection
}
// CaptureSelection 捕获正文与注解,不修改文档或历史
// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复
// 返回: *ObjectSelection 独立快照, error 标识或资源错误
func (e *Editor) CaptureSelection(page int, objects, annotations []string) (*ObjectSelection, error) {
items, err := e.Objects(page, objects)
if err != nil {
return nil, err
}
selection := &ObjectSelection{editor: e, document: e.documentIdentity(), objects: items}
if len(annotations) != 0 {
selection.annotations, err = e.CaptureAnnotations(page, annotations)
if err != nil {
return nil, err
}
}
return selection, nil
}
// PasteSelection 原子粘贴正文与注解并平移,失败回滚,一次撤销恢复全部
// 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
// 返回: []string 新正文标识, []string 新注解标识, error 标识、资源或编辑错误
func (e *Editor) PasteSelection(page int, selection *ObjectSelection, dx, dy float64) ([]string, []string, error) {
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) {
return nil, nil, fmt.Errorf("invalid object selection or offset")
}
var objects, annotations []string
err := e.Transaction(func(edit *Editor) error {
var err error
objects, err = edit.CopyObjects(page, selection.objects, dx, dy)
if err == nil && selection.annotations != nil {
annotations, err = edit.PasteAnnotations(page, selection.annotations, dx, dy)
}
return err
})
if err != nil {
return nil, nil, err
}
return objects, annotations, nil
}
// PasteSelectionToComposite 将正文快照粘贴至内部容器,保留源外观,拒绝嵌套注解
// 入参: page 目标页面索引, path 父复合路径, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
// 返回: []int 新成员序号, error 选区、资源或编辑错误
func (e *Editor) PasteSelectionToComposite(page int, path ObjectPath, selection *ObjectSelection, dx, dy float64) ([]int, error) {
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) {
return nil, fmt.Errorf("object selection belongs to another document or editor")
}
if selection.annotations != nil {
return nil, fmt.Errorf("annotations cannot be nested in a page object")
}
return e.CopyObjectsToComposite(page, path, selection.objects, dx, dy)
}
+179
View File
@@ -19,12 +19,33 @@ import (
"sync"
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
)
// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
var canvasFontLoadMu sync.Mutex
// textGlyphPathCacheKey 字形路径缓存键
type textGlyphPathCacheKey struct {
font *canvas.Font
size float64
fauxBold float64
fauxItalic float64
xOffset int32
yOffset int32
language string
script canvastext.Script
direction canvastext.Direction
glyph textGlyph
}
// textGlyphPathCacheValue 字形路径缓存值
type textGlyphPathCacheValue struct {
path *canvas.Path
width float64
}
// loadCanvasFont 串行加载绘图字体,不改动原始字体数据
// 入参: data 字体数据, index 集合索引, style 字体样式
// 返回: *canvas.Font 绘图字体, error 加载错误
@@ -58,3 +79,161 @@ func decodeFontContainer(data []byte) ([]byte, error) {
}
return basefont.SerializeOTF(loaded.Tables)
}
// loadFont 加载字体,绘图别名按数据和样式隔离,不改动原始字体名称
// 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if ff, ok := r.canvasState().fontMap[fontID]; ok {
return ff
}
resolved, err := r.PrepareFont(fontID)
if err != nil {
r.renderError = err
return nil
}
of := r.Reader.fontDefinition(fontID)
if len(resolved.Data) == 0 {
return nil
}
if of == nil {
of = &Font{ID: fontID, FontName: "OFDGoFallback"}
}
fontData := resolved.Data
fontStyle := canvasFontStyle(of)
if resolved.digest == ([32]byte{}) {
resolved.digest = r.fontDigest(fontData)
}
key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle}
cache := r.canvasFontFamilies()
if ff, ok := cache.get(key); ok {
r.canvasState().fontMap[fontID] = ff
return ff
}
ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle))
canvasFontLoadMu.Lock()
err = ff.LoadFont(fontData, 0, fontStyle)
canvasFontLoadMu.Unlock()
if err != nil {
r.renderError = err
return nil
}
r.canvasState().fontMap[fontID] = ff
cache.put(key, ff, min(len(fontData), cache.limit)*8+256)
return ff
}
// canvasFontStyle 获取Canvas字体样式
// 入参: font OFD字体定义
// 返回: canvas.FontStyle Canvas字体样式
func canvasFontStyle(font *Font) canvas.FontStyle {
var style canvas.FontStyle
if font.Bold {
style |= canvas.FontBold
}
if font.Italic {
style |= canvas.FontItalic
}
return style
}
// textGlyphWidth 获取绘制字形宽度
// 入参: face 字体, glyph 绘制字形
// 返回: float64 字形宽度
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
}
return face.TextWidth(glyph.Text)
}
// textGlyphPath 获取绘制字形路径
// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器
// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误
func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) {
p := &canvas.Path{}
width := 0.0
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
x, y := face.XOffset, face.YOffset
for _, shaped := range face.Glyphs(glyph.Text) {
part := &canvas.Path{}
if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil {
return nil, 0, err
}
p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset)))
x, y = x+shaped.XAdvance, y+shaped.YAdvance
}
width = face.MmPerEm * float64(x)
} else {
glyphID := uint16(glyph.GlyphID)
if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil {
return nil, 0, err
}
width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
}
if face.FauxBold != 0 {
p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance)
}
if face.FauxItalic != 0 {
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
}
return p, width, nil
}
// cachedTextGlyphPath 获取缓存的字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
key := textGlyphPathCacheKey{
font: face.Font,
size: face.Size,
fauxBold: face.FauxBold,
fauxItalic: face.FauxItalic,
xOffset: face.XOffset,
yOffset: face.YOffset,
language: face.Language,
script: face.Script,
direction: face.Direction,
glyph: glyph,
}
if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok {
return cached.path, cached.width
}
outlines := r.canvasState().fontOutlines[face.Font]
if outlines == nil {
cache := r.canvasFontOutlines()
outlines, _ = cache.get(face.Font)
if outlines == nil {
metrics, err := basefont.ParseSFNTTables(face.Font.Tables)
if err != nil {
r.renderError = err
return &canvas.Path{}, 0
}
outlines = &basefont.Outliner{Font: metrics}
cost := uint64(512)
for _, data := range metrics.Tables {
cost += uint64(len(data)) * 8
}
cost += uint64(metrics.CmapEntries()) * 48
cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1)))
}
r.canvasState().fontOutlines[face.Font] = outlines
}
path, width, err := textGlyphPath(face, glyph, outlines)
if err != nil {
r.renderError = err
return &canvas.Path{}, 0
}
r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language))
return path, width
}
// textMatrix 获取文本字形变换矩阵
// 入参: ctm OFD变换矩阵
// 返回: canvas.Matrix 画布变换矩阵
func textMatrix(ctm Matrix) canvas.Matrix {
return canvas.Matrix{
{ctm.a, -ctm.c, 0},
{-ctm.b, ctm.d, 0},
}
}
-78
View File
@@ -1,78 +0,0 @@
// 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 (
"fmt"
"github.com/tdewolff/canvas"
)
// loadFont 加载字体,绘图别名按数据和样式隔离,不改动原始字体名称
// 入参: fontID 字体ID
// 返回: *canvas.FontFamily 字体族
func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
if ff, ok := r.canvasState().fontMap[fontID]; ok {
return ff
}
resolved, err := r.PrepareFont(fontID)
if err != nil {
r.renderError = err
return nil
}
of := r.Reader.fontDefinition(fontID)
if len(resolved.Data) == 0 {
return nil
}
if of == nil {
of = &Font{ID: fontID, FontName: "OFDGoFallback"}
}
fontData := resolved.Data
fontStyle := canvasFontStyle(of)
if resolved.digest == ([32]byte{}) {
resolved.digest = r.fontDigest(fontData)
}
key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle}
cache := r.canvasFontFamilies()
if ff, ok := cache.get(key); ok {
r.canvasState().fontMap[fontID] = ff
return ff
}
ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle))
canvasFontLoadMu.Lock()
err = ff.LoadFont(fontData, 0, fontStyle)
canvasFontLoadMu.Unlock()
if err != nil {
r.renderError = err
return nil
}
r.canvasState().fontMap[fontID] = ff
cache.put(key, ff, min(len(fontData), cache.limit)*8+256)
return ff
}
// canvasFontStyle 获取Canvas字体样式
// 入参: font OFD字体定义
// 返回: canvas.FontStyle Canvas字体样式
func canvasFontStyle(font *Font) canvas.FontStyle {
var style canvas.FontStyle
if font.Bold {
style |= canvas.FontBold
}
if font.Italic {
style |= canvas.FontItalic
}
return style
}
-142
View File
@@ -1,142 +0,0 @@
// 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 (
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
)
// textGlyphPathCacheKey 字形路径缓存键
type textGlyphPathCacheKey struct {
font *canvas.Font
size float64
fauxBold float64
fauxItalic float64
xOffset int32
yOffset int32
language string
script canvastext.Script
direction canvastext.Direction
glyph textGlyph
}
// textGlyphPathCacheValue 字形路径缓存值
type textGlyphPathCacheValue struct {
path *canvas.Path
width float64
}
// textGlyphWidth 获取绘制字形宽度
// 入参: face 字体, glyph 绘制字形
// 返回: float64 字形宽度
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
}
return face.TextWidth(glyph.Text)
}
// textGlyphPath 获取绘制字形路径
// 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器
// 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误
func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) {
p := &canvas.Path{}
width := 0.0
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
x, y := face.XOffset, face.YOffset
for _, shaped := range face.Glyphs(glyph.Text) {
part := &canvas.Path{}
if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil {
return nil, 0, err
}
p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset)))
x, y = x+shaped.XAdvance, y+shaped.YAdvance
}
width = face.MmPerEm * float64(x)
} else {
glyphID := uint16(glyph.GlyphID)
if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil {
return nil, 0, err
}
width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
}
if face.FauxBold != 0 {
p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance)
}
if face.FauxItalic != 0 {
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
}
return p, width, nil
}
// cachedTextGlyphPath 获取缓存的字形路径
// 入参: face 字体, glyph 绘制字形
// 返回: *canvas.Path 字形路径, float64 字形宽度
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
key := textGlyphPathCacheKey{
font: face.Font,
size: face.Size,
fauxBold: face.FauxBold,
fauxItalic: face.FauxItalic,
xOffset: face.XOffset,
yOffset: face.YOffset,
language: face.Language,
script: face.Script,
direction: face.Direction,
glyph: glyph,
}
if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok {
return cached.path, cached.width
}
outlines := r.canvasState().fontOutlines[face.Font]
if outlines == nil {
cache := r.canvasFontOutlines()
outlines, _ = cache.get(face.Font)
if outlines == nil {
metrics, err := basefont.ParseSFNTTables(face.Font.Tables)
if err != nil {
r.renderError = err
return &canvas.Path{}, 0
}
outlines = &basefont.Outliner{Font: metrics}
cost := uint64(512)
for _, data := range metrics.Tables {
cost += uint64(len(data)) * 8
}
cost += uint64(metrics.CmapEntries()) * 48
cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1)))
}
r.canvasState().fontOutlines[face.Font] = outlines
}
path, width, err := textGlyphPath(face, glyph, outlines)
if err != nil {
r.renderError = err
return &canvas.Path{}, 0
}
r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language))
return path, width
}
// textMatrix 获取文本字形变换矩阵
// 入参: ctm OFD变换矩阵
// 返回: canvas.Matrix 画布变换矩阵
func textMatrix(ctm Matrix) canvas.Matrix {
return canvas.Matrix{
{ctm.a, -ctm.c, 0},
{-ctm.b, ctm.d, 0},
}
}
-119
View File
@@ -1,119 +0,0 @@
// 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 "github.com/tdewolff/canvas"
// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器
type canvasPageVisitor struct {
renderer *Renderer
context *canvas.Context
height float64
}
// DrawObject 绘制叶子对象,转换页面坐标裁剪
// 入参: object 源对象, state 继承状态
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
if state.Hidden {
return nil
}
clip, err := geometryToCanvasPath(state.Clip)
if err != nil {
return err
}
if clip != nil {
clip = clip.Translate(0, v.height)
}
switch object.Type {
case "TextObject":
v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "PathObject":
v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "ImageObject":
v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip)
}
return v.renderer.renderError
}
// DrawStamp 绘制签章外观
// 入参: stamp 签章
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error {
v.renderer.renderStamp(v.context, stamp, v.height)
return v.renderer.renderError
}
// BeginObject 开始可编辑对象分组
// 入参: object 源对象
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool {
groups, ok := v.context.Renderer.(interface {
beginObject(*GraphicObject) bool
endObject()
})
return ok && groups.beginObject(object)
}
// BeginAnnotation 开始注解分组
// 入参: id 注解标识
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginAnnotation(id string) bool {
groups, ok := v.context.Renderer.(interface {
beginAnnotation(string) bool
endObject()
})
return ok && groups.beginAnnotation(id)
}
// EndObject 结束对象或注解分组
func (v *canvasPageVisitor) EndObject() {
v.context.Renderer.(interface{ endObject() }).endObject()
}
// SkipObjects 保留不可见对象的编号
// 入参: count 对象数量
func (v *canvasPageVisitor) SkipObjects(count int) {
if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok {
groups.skipObjects(count)
}
}
// canvasRenderState 将Canvas裁剪转换为公共页面状态
// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
// 返回: RenderState 公共页面状态
func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState {
var clip *GeometryPath
if parentClip != nil {
clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH))
}
return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip}
}
// renderCompositeGraphicUnit 通过公共遍历绘制复合图元
// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil {
r.renderError = err
}
}
// renderObject 通过公共遍历绘制图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil {
r.renderError = err
}
}
+102
View File
@@ -18,6 +18,13 @@ import (
"github.com/tdewolff/canvas"
)
// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器
type canvasPageVisitor struct {
renderer *Renderer
context *canvas.Context
height float64
}
// renderCanvasPage 渲染特定页面内容
// 入参: page 页面内容
// 返回: *canvas.Canvas 画布实例, error 错误信息
@@ -74,3 +81,98 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont
}
return nil
}
// DrawObject 绘制叶子对象,转换页面坐标裁剪
// 入参: object 源对象, state 继承状态
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
if state.Hidden {
return nil
}
clip, err := geometryToCanvasPath(state.Clip)
if err != nil {
return err
}
if clip != nil {
clip = clip.Translate(0, v.height)
}
switch object.Type {
case "TextObject":
v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "PathObject":
v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip)
case "ImageObject":
v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip)
}
return v.renderer.renderError
}
// DrawStamp 绘制签章外观
// 入参: stamp 签章
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error {
v.renderer.renderStamp(v.context, stamp, v.height)
return v.renderer.renderError
}
// BeginObject 开始可编辑对象分组
// 入参: object 源对象
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool {
groups, ok := v.context.Renderer.(interface {
beginObject(*GraphicObject) bool
endObject()
})
return ok && groups.beginObject(object)
}
// BeginAnnotation 开始注解分组
// 入参: id 注解标识
// 返回: bool 是否建立分组
func (v *canvasPageVisitor) BeginAnnotation(id string) bool {
groups, ok := v.context.Renderer.(interface {
beginAnnotation(string) bool
endObject()
})
return ok && groups.beginAnnotation(id)
}
// EndObject 结束对象或注解分组
func (v *canvasPageVisitor) EndObject() {
v.context.Renderer.(interface{ endObject() }).endObject()
}
// SkipObjects 保留不可见对象的编号
// 入参: count 对象数量
func (v *canvasPageVisitor) SkipObjects(count int) {
if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok {
groups.skipObjects(count)
}
}
// canvasRenderState 将Canvas裁剪转换为公共页面状态
// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
// 返回: RenderState 公共页面状态
func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState {
var clip *GeometryPath
if parentClip != nil {
clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH))
}
return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip}
}
// renderCompositeGraphicUnit 通过公共遍历绘制复合图元
// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil {
r.renderError = err
}
}
// renderObject 通过公共遍历绘制图形对象
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil {
r.renderError = err
}
}
-43
View File
@@ -1,43 +0,0 @@
// 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 (
"fmt"
"strconv"
"strings"
)
// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立
type signatureIDSequence uint64
// observe 记录已有签章标识的编号上限,其他命名形式保持不变
// 入参: id 已有标识
func (s *signatureIDSequence) observe(id string) {
n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64)
if err == nil && n > uint64(*s) {
*s = signatureIDSequence(n)
}
}
// next 分配采用sNNN格式的签章标识
// 返回: string 新标识, error 编号耗尽错误
func (s *signatureIDSequence) next() (string, error) {
if uint64(*s) == ^uint64(0) {
return "", fmt.Errorf("signature ID exhausted")
}
*s++
return "s" + strconv.FormatUint(uint64(*s), 10), nil
}
+22
View File
@@ -76,6 +76,9 @@ type signatureWriteInfo struct {
Seal *SignatureSeal `xml:"Seal,omitempty"`
}
// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立
type signatureIDSequence uint64
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
// 含版本列表的文档需先通过ExtractVersion提取后签署
// 入参: data 最终包字节, options 签署选项
@@ -620,3 +623,22 @@ func signatureWriteZIP(parts map[string][]byte, compression CompressionOptions)
}
return out.Bytes(), nil
}
// observe 记录已有签章标识的编号上限,其他命名形式保持不变
// 入参: id 已有标识
func (s *signatureIDSequence) observe(id string) {
n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64)
if err == nil && n > uint64(*s) {
*s = signatureIDSequence(n)
}
}
// next 分配采用sNNN格式的签章标识
// 返回: string 新标识, error 编号耗尽错误
func (s *signatureIDSequence) next() (string, error) {
if uint64(*s) == ^uint64(0) {
return "", fmt.Errorf("signature ID exhausted")
}
*s++
return "s" + strconv.FormatUint(uint64(*s), 10), nil
}
-110
View File
@@ -1,110 +0,0 @@
// 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"
// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪
type structureObjectVisitor struct {
objects map[string]bool
targets []StructureTarget
listTargets bool
}
// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观
// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次
// 入参: page 从0开始的页面索引
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误
func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) {
return r.StructureObjectsContext(context.Background(), page)
}
// StructureObjectsContext 列出页面绑定候选,读取或遍历取消时不返回部分结果,不改变渲染选项
// 入参: ctx 取消上下文, page 从0开始的页面索引
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面读取、遍历或取消错误
func (r *Renderer) StructureObjectsContext(ctx context.Context, page int) ([]StructureTarget, error) {
content, err := r.Reader.PageContentByIndexContext(ctx, page)
if err != nil {
return nil, err
}
visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true}
view := *r
view.RenderAnnotations = true
view.pageContext = ctx
if err := view.WalkPage(content, visitor); err != nil {
return nil, err
}
for i := range visitor.targets {
if err := ctx.Err(); err != nil {
return nil, err
}
visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID
}
if err := ctx.Err(); err != nil {
return nil, err
}
return visitor.targets, nil
}
// BeginObject 登记图元标识,不创建分组状态
// 入参: object 页面图元
// 返回: bool 始终为false
func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool {
id := editorResourceID(editorObjectID(*object))
if id != "" && !v.objects[id] {
v.objects[id] = true
if !v.listTargets {
return false
}
target := StructureTarget{ID: id}
if object.Type == "TextObject" {
target.Text = object.TextObject.Text()
}
v.targets = append(v.targets, target)
}
return false
}
// EndObject 满足分组访问接口,不维护分组栈
func (v *structureObjectVisitor) EndObject() {}
// BeginAnnotation 不将注释容器编号作为图元标识
// 入参: id 注解标识
// 返回: bool 始终为false
func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false }
// SkipObjects 不依赖图元序号
// 入参: count 跳过数量
func (v *structureObjectVisitor) SkipObjects(count int) {}
// IncludeHidden 允许绑定隐藏图元
// 返回: bool 始终为true
func (v *structureObjectVisitor) IncludeHidden() bool { return true }
// SkipClips 绑定校验不需要裁剪轮廓
// 返回: bool 始终为true
func (v *structureObjectVisitor) SkipClips() bool { return true }
// DrawObject 不绘制图元,标识已由BeginObject登记
// 入参: object 页面图元, state 继承状态
// 返回: error 始终为nil
func (v *structureObjectVisitor) DrawObject(object *GraphicObject, state RenderState) error {
return nil
}
// DrawStamp 不将签章外观作为正文绑定目标
// 入参: stamp 签章外观
// 返回: error 始终为nil
func (v *structureObjectVisitor) DrawStamp(stamp Stamp) error { return nil }
+93
View File
@@ -29,6 +29,13 @@ type structureVisitor struct {
boundsOnly bool
}
// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪
type structureObjectVisitor struct {
objects map[string]bool
targets []StructureTarget
listTargets bool
}
// BeginObject 记录当前图元的标引目标,使复合图元引用覆盖其成员
// 入参: object 页面图元
// 返回: bool 是否需要配对结束通知
@@ -120,3 +127,89 @@ func (v *structureVisitor) DrawObject(object *GraphicObject, state RenderState)
// 入参: stamp 签章外观
// 返回: error 取消错误
func (v *structureVisitor) DrawStamp(stamp Stamp) error { return v.ctx.Err() }
// StructureObjects 列出指定页可绑定的图元及原文,包含模板、隐藏内容、复合图元成员和注释外观
// 不解码字体和图片,返回顺序遵循页面遍历顺序,同一标识仅列出一次
// 入参: page 从0开始的页面索引
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面或遍历错误
func (r *Renderer) StructureObjects(page int) ([]StructureTarget, error) {
return r.StructureObjectsContext(context.Background(), page)
}
// StructureObjectsContext 列出页面绑定候选,读取或遍历取消时不返回部分结果,不改变渲染选项
// 入参: ctx 取消上下文, page 从0开始的页面索引
// 返回: []StructureTarget 候选图元,不计算Bounds, error 页面读取、遍历或取消错误
func (r *Renderer) StructureObjectsContext(ctx context.Context, page int) ([]StructureTarget, error) {
content, err := r.Reader.PageContentByIndexContext(ctx, page)
if err != nil {
return nil, err
}
visitor := &structureObjectVisitor{objects: make(map[string]bool), listTargets: true}
view := *r
view.RenderAnnotations = true
view.pageContext = ctx
if err := view.WalkPage(content, visitor); err != nil {
return nil, err
}
for i := range visitor.targets {
if err := ctx.Err(); err != nil {
return nil, err
}
visitor.targets[i].Page, visitor.targets[i].PageID = page+1, content.ID
}
if err := ctx.Err(); err != nil {
return nil, err
}
return visitor.targets, nil
}
// BeginObject 登记图元标识,不创建分组状态
// 入参: object 页面图元
// 返回: bool 始终为false
func (v *structureObjectVisitor) BeginObject(object *GraphicObject) bool {
id := editorResourceID(editorObjectID(*object))
if id != "" && !v.objects[id] {
v.objects[id] = true
if !v.listTargets {
return false
}
target := StructureTarget{ID: id}
if object.Type == "TextObject" {
target.Text = object.TextObject.Text()
}
v.targets = append(v.targets, target)
}
return false
}
// EndObject 满足分组访问接口,不维护分组栈
func (v *structureObjectVisitor) EndObject() {}
// BeginAnnotation 不将注释容器编号作为图元标识
// 入参: id 注解标识
// 返回: bool 始终为false
func (v *structureObjectVisitor) BeginAnnotation(id string) bool { return false }
// SkipObjects 不依赖图元序号
// 入参: count 跳过数量
func (v *structureObjectVisitor) SkipObjects(count int) {}
// IncludeHidden 允许绑定隐藏图元
// 返回: bool 始终为true
func (v *structureObjectVisitor) IncludeHidden() bool { return true }
// SkipClips 绑定校验不需要裁剪轮廓
// 返回: bool 始终为true
func (v *structureObjectVisitor) SkipClips() bool { return true }
// DrawObject 不绘制图元,标识已由BeginObject登记
// 入参: object 页面图元, state 继承状态
// 返回: error 始终为nil
func (v *structureObjectVisitor) DrawObject(object *GraphicObject, state RenderState) error {
return nil
}
// DrawStamp 不将签章外观作为正文绑定目标
// 入参: stamp 签章外观
// 返回: error 始终为nil
func (v *structureObjectVisitor) DrawStamp(stamp Stamp) error { return nil }
-13
View File
@@ -266,19 +266,6 @@ func textCodePositioned(textCode TextCode, xs, ys textPositionValues) bool {
return multipleX || multipleY
}
// textDelta 获取文本偏移量
// 入参: deltas 偏移量数组, index 偏移量索引
// 返回: float64 偏移量, bool 是否存在
func textDelta(deltas []float64, index int) (float64, bool) {
if len(deltas) == 0 {
return 0, false
}
if index < len(deltas) {
return deltas[index], true
}
return deltas[len(deltas)-1], true
}
// textCodeXMLValue 将空格转义还原为字符,供XML写出层统一编码
// 入参: value OFD文字编码
// 返回: string 待写出的文字编码