mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
961bc52038
commit
d07972346b
19 files changed
+835
-1042
No files matched your search
@@ -21,6 +21,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SVG输出模式,控制资源内嵌和对象结构
|
// SVG输出模式,控制资源内嵌和对象结构
|
||||||
@@ -34,6 +35,12 @@ const (
|
|||||||
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
// ErrBackendUnavailable 表示未配置所需后端,不会自动切换其他实现
|
||||||
var ErrBackendUnavailable = errors.New("render backend unavailable")
|
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 提供稳定的后端标识,不包含前端文案
|
// Backend 提供稳定的后端标识,不包含前端文案
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
Name() string
|
Name() string
|
||||||
@@ -284,3 +291,48 @@ func immutableBackendValue(value reflect.Value) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
|
||||||
|
const compressionPatternImageLimit = 65536
|
||||||
|
|
||||||
// compressionImages 记录图片各处使用的最大像素需求
|
// compressionImages 记录图片各处使用的最大像素需求
|
||||||
type compressionImages struct {
|
type compressionImages struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -32,6 +35,12 @@ type compressionImages struct {
|
|||||||
patternVisits map[compressionPatternKey]bool
|
patternVisits map[compressionPatternKey]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求
|
||||||
|
type compressionPatternKey struct {
|
||||||
|
pattern *Pattern
|
||||||
|
linear [4]float64
|
||||||
|
}
|
||||||
|
|
||||||
// compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理
|
// compressionImagePlan 分文档合并图片需求,版本文件的当前入口独立于其他文档处理
|
||||||
// 入参: ctx 取消上下文, options 压缩配置
|
// 入参: ctx 取消上下文, options 压缩配置
|
||||||
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
|
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
|
||||||
@@ -275,3 +284,116 @@ func (v *compressionImages) IncludeHidden() bool { return true }
|
|||||||
// SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓
|
// SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓
|
||||||
// 返回: bool 始终为真
|
// 返回: bool 始终为真
|
||||||
func (v *compressionImages) SkipClips() bool { return true }
|
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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,14 +15,22 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// editorResourceScope 记录资源索引的文档归属链,引用集合只读
|
||||||
|
type editorResourceScope struct {
|
||||||
|
parent int
|
||||||
|
refs *editorResourceRefs
|
||||||
|
}
|
||||||
|
|
||||||
// scanEditorResources 优先扫描结构依赖,原始数据不按XML解析,孤立文件仍保留引用检查
|
// scanEditorResources 优先扫描结构依赖,原始数据不按XML解析,孤立文件仍保留引用检查
|
||||||
// 入参: ctx 取消上下文, reader 原包, parts 输出改动, generated 自产引用, names 扫描范围, progress 扫描进度, collect 是否保留各文件的资源引用
|
// 入参: ctx 取消上下文, reader 原包, parts 输出改动, generated 自产引用, names 扫描范围, progress 扫描进度, collect 是否保留各文件的资源引用
|
||||||
// 返回: *editorResourceRefs 全部引用, []string 资源索引, map[string]*editorResourceRefs 文件引用, bool 是否可安全优化, error 读取或取消错误
|
// 返回: *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()
|
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()
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,15 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ObjectSelection 保存正文与注解的独立快照,共享当前编辑器的不可变资源
|
||||||
|
// 来源修改、删除或撤销不改变快照,不用于跨文档传输
|
||||||
|
type ObjectSelection struct {
|
||||||
|
editor *Editor
|
||||||
|
document editorDocumentIdentity
|
||||||
|
objects []GraphicObject
|
||||||
|
annotations *AnnotationSelection
|
||||||
|
}
|
||||||
|
|
||||||
// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存
|
// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存
|
||||||
// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复
|
// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复
|
||||||
// 返回: Box 页面毫米范围, error 标识或度量错误
|
// 返回: Box 页面毫米范围, error 标识或度量错误
|
||||||
@@ -358,3 +367,56 @@ func (e *Editor) transformSelection(page int, objects, annotations []string, mat
|
|||||||
return nil
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,33 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
|
canvastext "github.com/tdewolff/canvas/text"
|
||||||
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
||||||
)
|
)
|
||||||
|
|
||||||
// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
|
// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
|
||||||
var canvasFontLoadMu sync.Mutex
|
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 串行加载绘图字体,不改动原始字体数据
|
// loadCanvasFont 串行加载绘图字体,不改动原始字体数据
|
||||||
// 入参: data 字体数据, index 集合索引, style 字体样式
|
// 入参: data 字体数据, index 集合索引, style 字体样式
|
||||||
// 返回: *canvas.Font 绘图字体, error 加载错误
|
// 返回: *canvas.Font 绘图字体, error 加载错误
|
||||||
@@ -58,3 +79,161 @@ func decodeFontContainer(data []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return basefont.SerializeOTF(loaded.Tables)
|
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},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,13 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器
|
||||||
|
type canvasPageVisitor struct {
|
||||||
|
renderer *Renderer
|
||||||
|
context *canvas.Context
|
||||||
|
height float64
|
||||||
|
}
|
||||||
|
|
||||||
// renderCanvasPage 渲染特定页面内容
|
// renderCanvasPage 渲染特定页面内容
|
||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
// 返回: *canvas.Canvas 画布实例, error 错误信息
|
// 返回: *canvas.Canvas 画布实例, error 错误信息
|
||||||
@@ -74,3 +81,98 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -76,6 +76,9 @@ type signatureWriteInfo struct {
|
|||||||
Seal *SignatureSeal `xml:"Seal,omitempty"`
|
Seal *SignatureSeal `xml:"Seal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立
|
||||||
|
type signatureIDSequence uint64
|
||||||
|
|
||||||
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
|
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
|
||||||
// 含版本列表的文档需先通过ExtractVersion提取后签署
|
// 含版本列表的文档需先通过ExtractVersion提取后签署
|
||||||
// 入参: data 最终包字节, options 签署选项
|
// 入参: data 最终包字节, options 签署选项
|
||||||
@@ -620,3 +623,22 @@ func signatureWriteZIP(parts map[string][]byte, compression CompressionOptions)
|
|||||||
}
|
}
|
||||||
return out.Bytes(), nil
|
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
|
||||||
|
}
|
||||||
@@ -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 }
|
|
||||||
@@ -29,6 +29,13 @@ type structureVisitor struct {
|
|||||||
boundsOnly bool
|
boundsOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// structureObjectVisitor 收集页面、复合图元及注释外观标识,绑定校验不解码字体、图片或裁剪
|
||||||
|
type structureObjectVisitor struct {
|
||||||
|
objects map[string]bool
|
||||||
|
targets []StructureTarget
|
||||||
|
listTargets bool
|
||||||
|
}
|
||||||
|
|
||||||
// BeginObject 记录当前图元的标引目标,使复合图元引用覆盖其成员
|
// BeginObject 记录当前图元的标引目标,使复合图元引用覆盖其成员
|
||||||
// 入参: object 页面图元
|
// 入参: object 页面图元
|
||||||
// 返回: bool 是否需要配对结束通知
|
// 返回: bool 是否需要配对结束通知
|
||||||
@@ -120,3 +127,89 @@ func (v *structureVisitor) DrawObject(object *GraphicObject, state RenderState)
|
|||||||
// 入参: stamp 签章外观
|
// 入参: stamp 签章外观
|
||||||
// 返回: error 取消错误
|
// 返回: error 取消错误
|
||||||
func (v *structureVisitor) DrawStamp(stamp Stamp) error { return v.ctx.Err() }
|
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 }
|
||||||
@@ -266,19 +266,6 @@ func textCodePositioned(textCode TextCode, xs, ys textPositionValues) bool {
|
|||||||
return multipleX || multipleY
|
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写出层统一编码
|
// textCodeXMLValue 将空格转义还原为字符,供XML写出层统一编码
|
||||||
// 入参: value OFD文字编码
|
// 入参: value OFD文字编码
|
||||||
// 返回: string 待写出的文字编码
|
// 返回: string 待写出的文字编码
|
||||||
|
|||||||
Reference in new issue
Block a user