diff --git a/ofdgo_compression_fonts.go b/ofdgo_compression_fonts.go
deleted file mode 100644
index 64a9ba8..0000000
--- a/ofdgo_compression_fonts.go
+++ /dev/null
@@ -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"
- "encoding/xml"
- "strings"
-)
-
-// compressionFontUsage 按文档合并共享字体用字,版本编辑仅统计选中入口
-// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 各文件引用, read 输出文件读取方法
-// 返回: map[string]*editorFontUsage 各字体文件的用字,无法确定作用域时为空, error 读取或取消错误
-func (r *Reader) compressionFontUsage(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, read func(string) ([]byte, error)) (map[string]*editorFontUsage, error) {
- selected := ""
- if r.OFD.DocBody[r.documentIndex].versioned {
- var err error
- selected, err = r.DocumentRootContext(ctx)
- if err != nil {
- return nil, err
- }
- }
- scoped, err := resourceReferenceScopes(ctx, documents, resources, references, selected)
- if err != nil || scoped == nil {
- return nil, err
- }
- return resourceFontUsage(ctx, resources, scoped, read)
-}
-
-// resourceFontUsage 按已确认的文档作用域合并各字体文件的字符和字形引用
-// 入参: ctx 取消上下文, resources 资源索引, scoped 各索引的用字范围, read 输出文件读取方法
-// 返回: map[string]*editorFontUsage 各字体文件的用字, error 读取或取消错误
-func resourceFontUsage(ctx context.Context, resources []string, scoped map[string]*editorResourceRefs, read func(string) ([]byte, error)) (map[string]*editorFontUsage, error) {
- result := make(map[string]*editorFontUsage)
- protected := make(map[string]bool)
- for _, name := range resources {
- if err := ctx.Err(); err != nil {
- return nil, err
- }
- refs := scoped[name]
- if refs == nil {
- continue
- }
- data, err := read(name)
- if err != nil {
- return nil, err
- }
- root, err := parseEditorXMLContext(ctx, data)
- if err != nil {
- return nil, err
- }
- for font := range editorResourceNodes(root, "Fonts", "Font") {
- if err := ctx.Err(); err != nil {
- return nil, err
- }
- file := font.child("FontFile")
- if file == nil {
- continue
- }
- var value string
- if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
- return nil, err
- }
- location := editorResourceLocation(name, root.attr("BaseLoc"), value)
- alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
- id, charset := editorResourceID(font.attr("ID")), font.attr("Charset")
- unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
- for i, path := range [2]string{location, alternate} {
- if i == 1 && path == location {
- continue
- }
- if result[path] == nil {
- result[path] = newEditorFontUsage()
- }
- result[path].merge(refs.fonts[id], unsafe)
- }
- }
- for media := range editorResourceNodes(root, "MultiMedias", "MultiMedia") {
- file := media.child("MediaFile")
- if file == nil {
- continue
- }
- var value string
- if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
- return nil, err
- }
- for _, location := range []string{editorResourceLocation(name, root.attr("BaseLoc"), value), cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))} {
- protected[location] = true
- }
- }
- }
- for location := range protected {
- if usage := result[location]; usage != nil {
- usage.unsafe = true
- }
- }
- return result, ctx.Err()
-}
diff --git a/ofdgo_compression_profiles.go b/ofdgo_compression_profiles.go
deleted file mode 100644
index cf5edf3..0000000
--- a/ofdgo_compression_profiles.go
+++ /dev/null
@@ -1,136 +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"
- "fmt"
- "maps"
- "slices"
-)
-
-// editorProfileReference 记录颜色配置文件所在的资源索引及颜色空间标识
-type editorProfileReference struct {
- index string
- id string
-}
-
-// compactResourceProfiles 合并相同颜色配置文件,保留颜色空间声明和独立文件引用
-// 入参: ctx 取消上下文, reader 原包,可为空, parts 输出改动, profiles 配置文件引用, protected 不可替换的文件, removed 待移除文件
-// 返回: error 读取、改写或取消错误
-func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[string][]byte, profiles map[string][]editorProfileReference, protected, removed map[string]bool) error {
- if len(profiles) < 2 {
- return ctx.Err()
- }
- seen := make(map[[32]byte]string)
- aliases := make(map[string]string)
- indexes := make(map[string]map[string]string)
- ordered := slices.Sorted(maps.Keys(profiles))
- sizes := make(map[string]uint64, len(profiles))
- counts := make(map[uint64]int)
- for _, name := range ordered {
- if err := ctx.Err(); err != nil {
- return err
- }
- if protected[name] {
- continue
- }
- size, err := outputResourceSize(reader, parts, name)
- if err != nil {
- return err
- }
- sizes[name] = size
- counts[size]++
- }
- var buffer []byte
- for _, name := range ordered {
- if err := ctx.Err(); err != nil {
- return err
- }
- if protected[name] || counts[sizes[name]] < 2 {
- continue
- }
- if buffer == nil {
- buffer = make([]byte, 32*1024)
- }
- key, err := outputResourceDigest(ctx, reader, parts, name, buffer)
- if err != nil {
- return err
- }
- first, found := seen[key]
- if !found {
- seen[key] = name
- continue
- }
- aliases[name] = first
- for _, reference := range profiles[name] {
- if indexes[reference.index] == nil {
- indexes[reference.index] = make(map[string]string)
- }
- indexes[reference.index][reference.id] = first
- }
- }
- updates := make(map[string][]byte, len(indexes))
- for _, name := range slices.Sorted(maps.Keys(indexes)) {
- if err := ctx.Err(); err != nil {
- return err
- }
- data, ok := parts[name]
- if !ok {
- if reader == nil {
- return fmt.Errorf("file not found: %s", name)
- }
- var err error
- data, err = reader.readFileViewContext(ctx, name)
- if err != nil {
- return err
- }
- }
- root, err := parseEditorXMLContext(ctx, data)
- if err != nil {
- return err
- }
- var patches []editorXMLPatch
- for _, group := range root.children {
- if !group.matchesOFD("ColorSpaces") {
- continue
- }
- for _, node := range group.children {
- if err := ctx.Err(); err != nil {
- return err
- }
- location := indexes[name][editorResourceID(node.attr("ID"))]
- if !node.matchesOFD("ColorSpace") || location == "" {
- continue
- }
- patch, err := editorXMLAttributePatch(data, node, "Profile", "/"+location)
- if err != nil {
- return err
- }
- patches = append(patches, patch)
- }
- }
- updates[name] = editorPatchXML(data, patches)
- }
- if err := ctx.Err(); err != nil {
- return err
- }
- maps.Copy(parts, updates)
- for name := range aliases {
- removed[name] = true
- delete(parts, name)
- }
- return nil
-}
diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go
index 560ef5f..b982c2c 100644
--- a/ofdgo_compression_resources.go
+++ b/ofdgo_compression_resources.go
@@ -35,6 +35,12 @@ type outputImageKey struct {
size image.Point
}
+// editorProfileReference 记录颜色配置文件所在的资源索引及颜色空间标识
+type editorProfileReference struct {
+ index string
+ id string
+}
+
// compressResourceParts 优化输出图片并合并相同资源文件,保留资源标识及各自属性
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
// 返回: error 读取或取消错误
@@ -470,3 +476,200 @@ func outputResourceDigest(ctx context.Context, reader *Reader, parts map[string]
}
return [32]byte(hash.Sum(nil)), ctx.Err()
}
+
+// compressionFontUsage 按文档合并共享字体用字,版本编辑仅统计选中入口
+// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 各文件引用, read 输出文件读取方法
+// 返回: map[string]*editorFontUsage 各字体文件的用字,无法确定作用域时为空, error 读取或取消错误
+func (r *Reader) compressionFontUsage(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, read func(string) ([]byte, error)) (map[string]*editorFontUsage, error) {
+ selected := ""
+ if r.OFD.DocBody[r.documentIndex].versioned {
+ var err error
+ selected, err = r.DocumentRootContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+ scoped, err := resourceReferenceScopes(ctx, documents, resources, references, selected)
+ if err != nil || scoped == nil {
+ return nil, err
+ }
+ return resourceFontUsage(ctx, resources, scoped, read)
+}
+
+// resourceFontUsage 按已确认的文档作用域合并各字体文件的字符和字形引用
+// 入参: ctx 取消上下文, resources 资源索引, scoped 各索引的用字范围, read 输出文件读取方法
+// 返回: map[string]*editorFontUsage 各字体文件的用字, error 读取或取消错误
+func resourceFontUsage(ctx context.Context, resources []string, scoped map[string]*editorResourceRefs, read func(string) ([]byte, error)) (map[string]*editorFontUsage, error) {
+ result := make(map[string]*editorFontUsage)
+ protected := make(map[string]bool)
+ for _, name := range resources {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ refs := scoped[name]
+ if refs == nil {
+ continue
+ }
+ data, err := read(name)
+ if err != nil {
+ return nil, err
+ }
+ root, err := parseEditorXMLContext(ctx, data)
+ if err != nil {
+ return nil, err
+ }
+ for font := range editorResourceNodes(root, "Fonts", "Font") {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ file := font.child("FontFile")
+ if file == nil {
+ continue
+ }
+ var value string
+ if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
+ return nil, err
+ }
+ location := editorResourceLocation(name, root.attr("BaseLoc"), value)
+ alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
+ id, charset := editorResourceID(font.attr("ID")), font.attr("Charset")
+ unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
+ for i, path := range [2]string{location, alternate} {
+ if i == 1 && path == location {
+ continue
+ }
+ if result[path] == nil {
+ result[path] = newEditorFontUsage()
+ }
+ result[path].merge(refs.fonts[id], unsafe)
+ }
+ }
+ for media := range editorResourceNodes(root, "MultiMedias", "MultiMedia") {
+ file := media.child("MediaFile")
+ if file == nil {
+ continue
+ }
+ var value string
+ if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
+ return nil, err
+ }
+ for _, location := range []string{editorResourceLocation(name, root.attr("BaseLoc"), value), cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))} {
+ protected[location] = true
+ }
+ }
+ }
+ for location := range protected {
+ if usage := result[location]; usage != nil {
+ usage.unsafe = true
+ }
+ }
+ return result, ctx.Err()
+}
+
+// compactResourceProfiles 合并相同颜色配置文件,保留颜色空间声明和独立文件引用
+// 入参: ctx 取消上下文, reader 原包,可为空, parts 输出改动, profiles 配置文件引用, protected 不可替换的文件, removed 待移除文件
+// 返回: error 读取、改写或取消错误
+func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[string][]byte, profiles map[string][]editorProfileReference, protected, removed map[string]bool) error {
+ if len(profiles) < 2 {
+ return ctx.Err()
+ }
+ seen := make(map[[32]byte]string)
+ aliases := make(map[string]string)
+ indexes := make(map[string]map[string]string)
+ ordered := slices.Sorted(maps.Keys(profiles))
+ sizes := make(map[string]uint64, len(profiles))
+ counts := make(map[uint64]int)
+ for _, name := range ordered {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if protected[name] {
+ continue
+ }
+ size, err := outputResourceSize(reader, parts, name)
+ if err != nil {
+ return err
+ }
+ sizes[name] = size
+ counts[size]++
+ }
+ var buffer []byte
+ for _, name := range ordered {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if protected[name] || counts[sizes[name]] < 2 {
+ continue
+ }
+ if buffer == nil {
+ buffer = make([]byte, 32*1024)
+ }
+ key, err := outputResourceDigest(ctx, reader, parts, name, buffer)
+ if err != nil {
+ return err
+ }
+ first, found := seen[key]
+ if !found {
+ seen[key] = name
+ continue
+ }
+ aliases[name] = first
+ for _, reference := range profiles[name] {
+ if indexes[reference.index] == nil {
+ indexes[reference.index] = make(map[string]string)
+ }
+ indexes[reference.index][reference.id] = first
+ }
+ }
+ updates := make(map[string][]byte, len(indexes))
+ for _, name := range slices.Sorted(maps.Keys(indexes)) {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ data, ok := parts[name]
+ if !ok {
+ if reader == nil {
+ return fmt.Errorf("file not found: %s", name)
+ }
+ var err error
+ data, err = reader.readFileViewContext(ctx, name)
+ if err != nil {
+ return err
+ }
+ }
+ root, err := parseEditorXMLContext(ctx, data)
+ if err != nil {
+ return err
+ }
+ var patches []editorXMLPatch
+ for _, group := range root.children {
+ if !group.matchesOFD("ColorSpaces") {
+ continue
+ }
+ for _, node := range group.children {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ location := indexes[name][editorResourceID(node.attr("ID"))]
+ if !node.matchesOFD("ColorSpace") || location == "" {
+ continue
+ }
+ patch, err := editorXMLAttributePatch(data, node, "Profile", "/"+location)
+ if err != nil {
+ return err
+ }
+ patches = append(patches, patch)
+ }
+ }
+ updates[name] = editorPatchXML(data, patches)
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ maps.Copy(parts, updates)
+ for name := range aliases {
+ removed[name] = true
+ delete(parts, name)
+ }
+ return nil
+}
diff --git a/ofdgo_convert.go b/ofdgo_convert.go
index f1e0fc5..54a102d 100644
--- a/ofdgo_convert.go
+++ b/ofdgo_convert.go
@@ -15,11 +15,17 @@
package ofdgo
import (
+ "archive/zip"
+ "bytes"
"context"
+ "errors"
"fmt"
"io"
+ "runtime"
"slices"
"strings"
+ "sync"
+ "time"
"github.com/xiaoqidun/pdfgo"
)
@@ -82,6 +88,26 @@ type ConvertReport struct {
Fonts []FontChange `json:"fonts,omitempty"`
}
+// ConversionPoolOptions 设置共享转换并行度及图像工作集预算
+// Parallelism为0时使用GOMAXPROCS,MemoryLimit为0时不指定固定预算
+// AvailableMemory返回可用于新增任务的字节数,nil表示不探测,回调须支持并发调用
+// 内存预算用于调度,不限制解析器及运行时的实际分配
+type ConversionPoolOptions struct {
+ Parallelism int
+ MemoryLimit int64
+ AvailableMemory func() int64
+}
+
+// ConversionPool 协调独立转换和图像任务,配置在创建后不变,可跨会话复用
+type ConversionPool struct {
+ options ConversionPoolOptions
+ mu sync.Mutex
+ changed chan struct{}
+ active int
+ images int
+ memory int64
+}
+
// convertReader 将取消上下文传递到随机读取
type convertReader struct {
ctx context.Context
@@ -95,6 +121,9 @@ type convertWriter struct {
count *int64
}
+// conversionPoolKey 标识单次转换使用的共享预算
+type conversionPoolKey struct{}
+
// OutputFormats 返回独立的输出格式列表,不依赖WebUI或具体渲染后端
// 返回: []OutputFormat 输出格式列表
func OutputFormats() []OutputFormat {
@@ -412,3 +441,158 @@ func (w convertWriter) WriteString(data string) (int, error) {
}
return n, err
}
+
+// DetectFormat 按文件内容识别PDF或OFD,不读取压缩内容或关闭输入
+// 识别结果不代表文档完整有效,仍需通过对应阅读器解析
+// 入参: input 输入源
+// 返回: string 格式标识pdf或ofd, error 读取错误或不支持的格式
+func DetectFormat(input *Input) (string, error) {
+ if input == nil || input.Size() <= 0 {
+ return "", fmt.Errorf("empty document input")
+ }
+ size := input.Size()
+ var header [1024]byte
+ data := header[:min(size, int64(len(header)))]
+ n, err := input.ReadAt(data, 0)
+ if err != nil && err != io.EOF {
+ return "", err
+ }
+ if n != len(data) {
+ return "", io.ErrUnexpectedEOF
+ }
+ if bytes.HasPrefix(data, []byte("%PDF-")) {
+ return "pdf", nil
+ }
+ archive, err := zip.NewReader(input, size)
+ if err == nil {
+ reader := &Reader{Zip: archive}
+ if err := reader.indexPackage(); err != nil {
+ return "", err
+ }
+ if reader.fileNames["OFD.xml"] || reader.fileNames["Encryptions.xml"] {
+ return "ofd", nil
+ }
+ } else {
+ if !errors.Is(err, zip.ErrFormat) {
+ return "", err
+ }
+ if bytes.Contains(data, []byte("%PDF-")) {
+ return "pdf", nil
+ }
+ }
+ return "", fmt.Errorf("unsupported input format")
+}
+
+// NewConversionPool 创建共享转换调度器,不启动后台任务
+// 入参: options 并行及内存选项
+// 返回: *ConversionPool 调度器, error 配置错误
+func NewConversionPool(options ConversionPoolOptions) (*ConversionPool, error) {
+ if options.Parallelism < 0 || options.MemoryLimit < 0 {
+ return nil, fmt.Errorf("invalid conversion pool options")
+ }
+ if options.Parallelism == 0 {
+ options.Parallelism = runtime.GOMAXPROCS(0)
+ }
+ return &ConversionPool{options: options, changed: make(chan struct{})}, nil
+}
+
+// Parallelism 返回同时运行的转换数量上限
+// 返回: int 并行上限
+func (p *ConversionPool) Parallelism() int { return p.options.Parallelism }
+
+// memoryLimit 读取当前可分配的图像工作量预算
+// 返回: int64 可用字节数,负值表示未设置预算
+func (p *ConversionPool) memoryLimit() int64 {
+ limit := int64(-1)
+ if p.options.MemoryLimit > 0 {
+ limit = p.options.MemoryLimit
+ }
+ if p.options.AvailableMemory != nil {
+ available := max(0, p.options.AvailableMemory())
+ if limit < 0 || available < limit {
+ limit = available
+ }
+ }
+ return limit
+}
+
+// acquire 等待转换或图像预算,超预算图像可独占运行
+// 入参: ctx 取消上下文, image 是否为图像任务, size 图像工作量
+// 返回: error 取消错误
+func (p *ConversionPool) acquire(ctx context.Context, image bool, size int64) error {
+ var timer *time.Timer
+ defer func() {
+ if timer != nil {
+ timer.Stop()
+ }
+ }()
+ for {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ limit := p.memoryLimit()
+ p.mu.Lock()
+ allowed := p.active < p.options.Parallelism && (p.active == 0 || limit != 0)
+ if image {
+ allowed = p.images < runtime.GOMAXPROCS(0) && (p.images == 0 || limit < 0 || size <= limit-p.memory)
+ }
+ if allowed {
+ if image {
+ p.images++
+ p.memory += size
+ } else {
+ p.active++
+ }
+ p.mu.Unlock()
+ return nil
+ }
+ changed := p.changed
+ p.mu.Unlock()
+ var tick <-chan time.Time
+ if p.options.AvailableMemory != nil {
+ if timer == nil {
+ timer = time.NewTimer(250 * time.Millisecond)
+ } else {
+ timer.Reset(250 * time.Millisecond)
+ }
+ tick = timer.C
+ }
+ select {
+ case <-ctx.Done():
+ case <-changed:
+ case <-tick:
+ }
+ if timer != nil {
+ timer.Stop()
+ }
+ }
+}
+
+// release 归还任务预算并唤醒等待者
+// 入参: image 是否为图像任务, size 图像工作量
+func (p *ConversionPool) release(image bool, size int64) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ if image {
+ p.images--
+ p.memory -= size
+ } else {
+ p.active--
+ }
+ close(p.changed)
+ p.changed = make(chan struct{})
+}
+
+// conversionWorkers 按当前文件数量分配内部计算任务
+// 入参: ctx 转换上下文, fallback 非共享模式并行上限
+// 返回: int 内部并行数
+func conversionWorkers(ctx context.Context, fallback int) int {
+ p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool)
+ if p == nil {
+ return min(fallback, runtime.GOMAXPROCS(0))
+ }
+ p.mu.Lock()
+ active := p.active
+ p.mu.Unlock()
+ return max(1, runtime.GOMAXPROCS(0)/max(1, active))
+}
diff --git a/ofdgo_convert_pool.go b/ofdgo_convert_pool.go
deleted file mode 100644
index 3ebcb41..0000000
--- a/ofdgo_convert_pool.go
+++ /dev/null
@@ -1,160 +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"
- "fmt"
- "runtime"
- "sync"
- "time"
-)
-
-// ConversionPoolOptions 设置共享转换并行度及图像工作集预算
-// Parallelism为0时使用GOMAXPROCS,MemoryLimit为0时不指定固定预算
-// AvailableMemory返回可用于新增任务的字节数,nil表示不探测,回调须支持并发调用
-// 内存预算用于调度,不限制解析器及运行时的实际分配
-type ConversionPoolOptions struct {
- Parallelism int
- MemoryLimit int64
- AvailableMemory func() int64
-}
-
-// ConversionPool 协调独立转换和图像任务,配置在创建后不变,可跨会话复用
-type ConversionPool struct {
- options ConversionPoolOptions
- mu sync.Mutex
- changed chan struct{}
- active int
- images int
- memory int64
-}
-
-// conversionPoolKey 标识单次转换使用的共享预算
-type conversionPoolKey struct{}
-
-// NewConversionPool 创建共享转换调度器,不启动后台任务
-// 入参: options 并行及内存选项
-// 返回: *ConversionPool 调度器, error 配置错误
-func NewConversionPool(options ConversionPoolOptions) (*ConversionPool, error) {
- if options.Parallelism < 0 || options.MemoryLimit < 0 {
- return nil, fmt.Errorf("invalid conversion pool options")
- }
- if options.Parallelism == 0 {
- options.Parallelism = runtime.GOMAXPROCS(0)
- }
- return &ConversionPool{options: options, changed: make(chan struct{})}, nil
-}
-
-// Parallelism 返回同时运行的转换数量上限
-// 返回: int 并行上限
-func (p *ConversionPool) Parallelism() int { return p.options.Parallelism }
-
-// memoryLimit 读取当前可分配的图像工作量预算
-// 返回: int64 可用字节数,负值表示未设置预算
-func (p *ConversionPool) memoryLimit() int64 {
- limit := int64(-1)
- if p.options.MemoryLimit > 0 {
- limit = p.options.MemoryLimit
- }
- if p.options.AvailableMemory != nil {
- available := max(0, p.options.AvailableMemory())
- if limit < 0 || available < limit {
- limit = available
- }
- }
- return limit
-}
-
-// acquire 等待转换或图像预算,超预算图像可独占运行
-// 入参: ctx 取消上下文, image 是否为图像任务, size 图像工作量
-// 返回: error 取消错误
-func (p *ConversionPool) acquire(ctx context.Context, image bool, size int64) error {
- var timer *time.Timer
- defer func() {
- if timer != nil {
- timer.Stop()
- }
- }()
- for {
- if err := ctx.Err(); err != nil {
- return err
- }
- limit := p.memoryLimit()
- p.mu.Lock()
- allowed := p.active < p.options.Parallelism && (p.active == 0 || limit != 0)
- if image {
- allowed = p.images < runtime.GOMAXPROCS(0) && (p.images == 0 || limit < 0 || size <= limit-p.memory)
- }
- if allowed {
- if image {
- p.images++
- p.memory += size
- } else {
- p.active++
- }
- p.mu.Unlock()
- return nil
- }
- changed := p.changed
- p.mu.Unlock()
- var tick <-chan time.Time
- if p.options.AvailableMemory != nil {
- if timer == nil {
- timer = time.NewTimer(250 * time.Millisecond)
- } else {
- timer.Reset(250 * time.Millisecond)
- }
- tick = timer.C
- }
- select {
- case <-ctx.Done():
- case <-changed:
- case <-tick:
- }
- if timer != nil {
- timer.Stop()
- }
- }
-}
-
-// release 归还任务预算并唤醒等待者
-// 入参: image 是否为图像任务, size 图像工作量
-func (p *ConversionPool) release(image bool, size int64) {
- p.mu.Lock()
- defer p.mu.Unlock()
- if image {
- p.images--
- p.memory -= size
- } else {
- p.active--
- }
- close(p.changed)
- p.changed = make(chan struct{})
-}
-
-// conversionWorkers 按当前文件数量分配内部计算任务
-// 入参: ctx 转换上下文, fallback 非共享模式并行上限
-// 返回: int 内部并行数
-func conversionWorkers(ctx context.Context, fallback int) int {
- p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool)
- if p == nil {
- return min(fallback, runtime.GOMAXPROCS(0))
- }
- p.mu.Lock()
- active := p.active
- p.mu.Unlock()
- return max(1, runtime.GOMAXPROCS(0)/max(1, active))
-}
diff --git a/ofdgo_crypto.go b/ofdgo_crypto.go
index 87c51cb..37746a3 100644
--- a/ofdgo_crypto.go
+++ b/ofdgo_crypto.go
@@ -18,14 +18,7 @@ import (
"bytes"
"crypto"
"errors"
-)
-
-// 加密错误可通过errors.Is识别,不包含口令或私钥
-var (
- ErrCredentialsRequired = errors.New("document credentials required")
- ErrInvalidCredentials = errors.New("invalid credentials or damaged encrypted document")
- ErrUnsupportedEncryption = errors.New("unsupported document encryption")
- ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
+ "io"
)
// Credentials 解密凭据,UserName用于筛选接收者,留空自动匹配,不参与口令派生;证书采用DER编码
@@ -51,12 +44,6 @@ type CryptoProvider interface {
// ReaderOption 阅读器的凭据与密码提供者选项
type ReaderOption func(*readerOptions)
-// readerOptions 保存打开文档时的会话参数
-type readerOptions struct {
- credentials []Credentials
- provider CryptoProvider
-}
-
// EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码
type EncryptionRecipient struct {
UserName string
@@ -85,12 +72,26 @@ type EncryptionInfo struct {
PolicyRequired bool
}
+// readerOptions 保存打开文档时的会话参数
+type readerOptions struct {
+ credentials []Credentials
+ provider CryptoProvider
+}
+
// encryptionState 保存解密来源与可继承的输出策略
type encryptionState struct {
info EncryptionInfo
options *EncryptionOptions
}
+// 加密错误可通过errors.Is识别,不包含口令或私钥
+var (
+ ErrCredentialsRequired = errors.New("document credentials required")
+ ErrInvalidCredentials = errors.New("invalid credentials or damaged encrypted document")
+ ErrUnsupportedEncryption = errors.New("unsupported document encryption")
+ ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
+)
+
// WithCredentials 添加解密凭据,多次调用可解开多层加密
// 入参: credentials 解密凭据
// 返回: ReaderOption 阅读选项
@@ -145,3 +146,69 @@ func cloneEncryptionOptions(options *EncryptionOptions) *EncryptionOptions {
}
return ©
}
+
+// Encryption 获取当前保存策略,不暴露口令或私钥
+// 返回: EncryptionInfo 加密状态
+func (e *Editor) Encryption() EncryptionInfo {
+ return e.encryption.summary()
+}
+
+// SetEncryption 设置后续保存的加密策略,nil显式选择明文保存
+// 打开单用户口令或证书加密文档时默认继承原策略;多人口令和多层加密需显式选择输出策略
+// 新的加密密钥与初始向量在每次保存时独立生成,设置不改变文档内容或撤销历史
+// 入参: options 加密策略
+// 返回: error 错误信息
+func (e *Editor) SetEncryption(options *EncryptionOptions) error {
+ if options == nil {
+ e.encryption = nil
+ return nil
+ }
+ if err := validateEncryptionOptions(*options); err != nil {
+ return err
+ }
+ info := EncryptionInfo{Encrypted: true, Method: "1.1.1", Layers: 1}
+ if len(options.Password) != 0 {
+ info.Users = []string{options.UserName}
+ } else {
+ info.Method = "1.1.2"
+ for _, recipient := range options.Recipients {
+ info.Users = append(info.Users, recipient.UserName)
+ }
+ }
+ e.encryption = &encryptionState{info: info, options: cloneEncryptionOptions(options)}
+ return nil
+}
+
+// writeEncrypted 在全部准备及加密成功后写出结果,取消不产生输出
+// 输出流自身写入失败时调用方仍需丢弃本次输出
+// 入参: writer 输出流
+// 返回: int64 写入字节数, error 错误信息
+func (e *Editor) writeEncrypted(writer io.Writer) (int64, error) {
+ if e.encryption.options == nil {
+ return 0, ErrEncryptionPolicyRequired
+ }
+ var plaintext bytes.Buffer
+ if _, err := e.writePlaintext(&plaintext); err != nil {
+ return 0, err
+ }
+ defer clear(plaintext.Bytes())
+ r, err := NewReader(&Input{reader: bytes.NewReader(plaintext.Bytes()), size: int64(plaintext.Len())})
+ if err != nil {
+ return 0, err
+ }
+ defer r.Close()
+ parts, err := r.packageData()
+ if err != nil {
+ return 0, err
+ }
+ defer func() {
+ for _, data := range parts {
+ clear(data)
+ }
+ }()
+ data, err := encryptPackageParts(parts, *e.encryption.options, editorProgress(e.OnWriteProgress))
+ if err != nil {
+ return 0, err
+ }
+ return io.Copy(writer, bytes.NewReader(data))
+}
diff --git a/ofdgo_crypto_editor.go b/ofdgo_crypto_editor.go
deleted file mode 100644
index cb4f34e..0000000
--- a/ofdgo_crypto_editor.go
+++ /dev/null
@@ -1,86 +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 (
- "bytes"
- "io"
-)
-
-// Encryption 获取当前保存策略,不暴露口令或私钥
-// 返回: EncryptionInfo 加密状态
-func (e *Editor) Encryption() EncryptionInfo {
- return e.encryption.summary()
-}
-
-// SetEncryption 设置后续保存的加密策略,nil显式选择明文保存
-// 打开单用户口令或证书加密文档时默认继承原策略;多人口令和多层加密需显式选择输出策略
-// 新的加密密钥与初始向量在每次保存时独立生成,设置不改变文档内容或撤销历史
-// 入参: options 加密策略
-// 返回: error 错误信息
-func (e *Editor) SetEncryption(options *EncryptionOptions) error {
- if options == nil {
- e.encryption = nil
- return nil
- }
- if err := validateEncryptionOptions(*options); err != nil {
- return err
- }
- info := EncryptionInfo{Encrypted: true, Method: "1.1.1", Layers: 1}
- if len(options.Password) != 0 {
- info.Users = []string{options.UserName}
- } else {
- info.Method = "1.1.2"
- for _, recipient := range options.Recipients {
- info.Users = append(info.Users, recipient.UserName)
- }
- }
- e.encryption = &encryptionState{info: info, options: cloneEncryptionOptions(options)}
- return nil
-}
-
-// writeEncrypted 在全部准备及加密成功后写出结果,取消不产生输出
-// 输出流自身写入失败时调用方仍需丢弃本次输出
-// 入参: writer 输出流
-// 返回: int64 写入字节数, error 错误信息
-func (e *Editor) writeEncrypted(writer io.Writer) (int64, error) {
- if e.encryption.options == nil {
- return 0, ErrEncryptionPolicyRequired
- }
- var plaintext bytes.Buffer
- if _, err := e.writePlaintext(&plaintext); err != nil {
- return 0, err
- }
- defer clear(plaintext.Bytes())
- r, err := NewReader(&Input{reader: bytes.NewReader(plaintext.Bytes()), size: int64(plaintext.Len())})
- if err != nil {
- return 0, err
- }
- defer r.Close()
- parts, err := r.packageData()
- if err != nil {
- return 0, err
- }
- defer func() {
- for _, data := range parts {
- clear(data)
- }
- }()
- data, err := encryptPackageParts(parts, *e.encryption.options, editorProgress(e.OnWriteProgress))
- if err != nil {
- return 0, err
- }
- return io.Copy(writer, bytes.NewReader(data))
-}
diff --git a/ofdgo_font_metrics.go b/ofdgo_font_metrics.go
index eee4164..2614472 100644
--- a/ofdgo_font_metrics.go
+++ b/ofdgo_font_metrics.go
@@ -15,6 +15,7 @@
package ofdgo
import (
+ "encoding/binary"
"fmt"
"sync"
@@ -24,6 +25,34 @@ import (
// sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
const sfntGlyphBoundsLimit = 4096
+// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
+type FontResource struct {
+ FontMetrics
+ Data []byte
+ Font Font
+ Extension string
+ CanSubset bool
+}
+
+// FontUsage 描述全包已确认的字体引用,Unsafe禁止裁剪
+type FontUsage struct {
+ Characters []rune
+ Glyphs []uint16
+ Unsafe bool
+}
+
+// FontResourceBackend 解析和裁剪保存资源,独立于显示字体后端
+// SubsetFont的preserve为true时不得改变字形编号,SubsetSourceFont无安全收益时返回nil
+type FontResourceBackend interface {
+ Backend
+ OpenFontResource(file FontFile, index int) (*FontResource, error)
+ SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error)
+ SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error)
+}
+
+// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理
+type SFNTBackend struct{}
+
// sfntFontMetrics 保留默认字体度量,写出时不更新原始时间
type sfntFontMetrics struct {
*basefont.SFNT
@@ -47,6 +76,9 @@ type sfntGlyphSize struct {
// glyphGeometry 将字体轮廓转换为纵轴向下的几何路径
type glyphGeometry struct{ path GeometryPath }
+// sfntResourceMetrics 隔离资源解析器的度量实现
+type sfntResourceMetrics struct{ *basefont.SFNT }
+
// MoveTo 开始子路径
// 入参: x 横坐标, y 纵坐标
func (p *glyphGeometry) MoveTo(x, y float64) {
@@ -180,3 +212,64 @@ func (f sfntFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]Geometr
}
return result, nil
}
+
+// Name 返回资源后端标识
+// 返回: string 后端标识
+func (SFNTBackend) Name() string { return "sfnt" }
+
+// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力
+// CFF2使用默认实例的静态轮廓,不改变原字体文件
+// 入参: file 字体文件, index 集合索引
+// 返回: *FontResource 字体资源, error 解析错误
+func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) {
+ data, err := file.Face(index)
+ if err != nil {
+ return nil, err
+ }
+ data, err = basefont.DefaultCFF2Font(data)
+ if err != nil {
+ return nil, err
+ }
+ sfnt, err := basefont.ParseSFNT(data)
+ if err != nil {
+ return nil, err
+ }
+ name := basefont.SFNTName(sfnt, 6, 4, 1)
+ if name == "" {
+ return nil, fmt.Errorf("font has no name")
+ }
+ extension := ".ttf"
+ if sfnt.IsCFF {
+ extension = ".otf"
+ }
+ return &FontResource{
+ FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension,
+ Font: Font{FontName: name, FamilyName: basefont.SFNTName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0},
+ CanSubset: basefont.Subsettable(sfnt),
+ }, nil
+}
+
+// SubsetFont 裁剪新建字体并按要求保留显式字形编号
+// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号
+// 返回: []byte 子集, error 裁剪错误
+func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) {
+ return basefont.SubsetSFNT(data, glyphs, preserve)
+}
+
+// SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源
+// 入参: data 字体数据, usage 用字信息
+// 返回: []byte 子集, error 裁剪错误
+func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) {
+ u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe}
+ for _, char := range usage.Characters {
+ u.Characters[char] = true
+ }
+ for _, glyph := range usage.Glyphs {
+ u.Glyphs[glyph] = true
+ }
+ return basefont.SubsetSourceSFNT(data, u), nil
+}
+
+// Write 返回未改动元信息的字体数据
+// 返回: []byte 字体数据
+func (f sfntResourceMetrics) Write() []byte { return f.SFNT.Write() }
diff --git a/ofdgo_font_resource.go b/ofdgo_font_resource.go
deleted file mode 100644
index 22d6149..0000000
--- a/ofdgo_font_resource.go
+++ /dev/null
@@ -1,114 +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 (
- "encoding/binary"
- "fmt"
-
- basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
-)
-
-// FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型
-type FontResource struct {
- FontMetrics
- Data []byte
- Font Font
- Extension string
- CanSubset bool
-}
-
-// FontUsage 描述全包已确认的字体引用,Unsafe禁止裁剪
-type FontUsage struct {
- Characters []rune
- Glyphs []uint16
- Unsafe bool
-}
-
-// FontResourceBackend 解析和裁剪保存资源,独立于显示字体后端
-// SubsetFont的preserve为true时不得改变字形编号,SubsetSourceFont无安全收益时返回nil
-type FontResourceBackend interface {
- Backend
- OpenFontResource(file FontFile, index int) (*FontResource, error)
- SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error)
- SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error)
-}
-
-// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理
-type SFNTBackend struct{}
-
-// sfntResourceMetrics 隔离资源解析器的度量实现
-type sfntResourceMetrics struct{ *basefont.SFNT }
-
-// Name 返回资源后端标识
-// 返回: string 后端标识
-func (SFNTBackend) Name() string { return "sfnt" }
-
-// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力
-// CFF2使用默认实例的静态轮廓,不改变原字体文件
-// 入参: file 字体文件, index 集合索引
-// 返回: *FontResource 字体资源, error 解析错误
-func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) {
- data, err := file.Face(index)
- if err != nil {
- return nil, err
- }
- data, err = basefont.DefaultCFF2Font(data)
- if err != nil {
- return nil, err
- }
- sfnt, err := basefont.ParseSFNT(data)
- if err != nil {
- return nil, err
- }
- name := basefont.SFNTName(sfnt, 6, 4, 1)
- if name == "" {
- return nil, fmt.Errorf("font has no name")
- }
- extension := ".ttf"
- if sfnt.IsCFF {
- extension = ".otf"
- }
- return &FontResource{
- FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension,
- Font: Font{FontName: name, FamilyName: basefont.SFNTName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0},
- CanSubset: basefont.Subsettable(sfnt),
- }, nil
-}
-
-// SubsetFont 裁剪新建字体并按要求保留显式字形编号
-// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号
-// 返回: []byte 子集, error 裁剪错误
-func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) {
- return basefont.SubsetSFNT(data, glyphs, preserve)
-}
-
-// SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源
-// 入参: data 字体数据, usage 用字信息
-// 返回: []byte 子集, error 裁剪错误
-func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) {
- u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe}
- for _, char := range usage.Characters {
- u.Characters[char] = true
- }
- for _, glyph := range usage.Glyphs {
- u.Glyphs[glyph] = true
- }
- return basefont.SubsetSourceSFNT(data, u), nil
-}
-
-// Write 返回未改动元信息的字体数据
-// 返回: []byte 字体数据
-func (f sfntResourceMetrics) Write() []byte { return f.SFNT.Write() }
diff --git a/ofdgo_format.go b/ofdgo_format.go
deleted file mode 100644
index 5a9378b..0000000
--- a/ofdgo_format.go
+++ /dev/null
@@ -1,64 +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 (
- "archive/zip"
- "bytes"
- "errors"
- "fmt"
- "io"
-)
-
-// DetectFormat 按文件内容识别PDF或OFD,不读取压缩内容或关闭输入
-// 识别结果不代表文档完整有效,仍需通过对应阅读器解析
-// 入参: input 输入源
-// 返回: string 格式标识pdf或ofd, error 读取错误或不支持的格式
-func DetectFormat(input *Input) (string, error) {
- if input == nil || input.Size() <= 0 {
- return "", fmt.Errorf("empty document input")
- }
- size := input.Size()
- var header [1024]byte
- data := header[:min(size, int64(len(header)))]
- n, err := input.ReadAt(data, 0)
- if err != nil && err != io.EOF {
- return "", err
- }
- if n != len(data) {
- return "", io.ErrUnexpectedEOF
- }
- if bytes.HasPrefix(data, []byte("%PDF-")) {
- return "pdf", nil
- }
- archive, err := zip.NewReader(input, size)
- if err == nil {
- reader := &Reader{Zip: archive}
- if err := reader.indexPackage(); err != nil {
- return "", err
- }
- if reader.fileNames["OFD.xml"] || reader.fileNames["Encryptions.xml"] {
- return "ofd", nil
- }
- } else {
- if !errors.Is(err, zip.ErrFormat) {
- return "", err
- }
- if bytes.Contains(data, []byte("%PDF-")) {
- return "pdf", nil
- }
- }
- return "", fmt.Errorf("unsupported input format")
-}
diff --git a/ofdgo_mesh_sampler.go b/ofdgo_mesh_sampler.go
deleted file mode 100644
index fe22d68..0000000
--- a/ofdgo_mesh_sampler.go
+++ /dev/null
@@ -1,120 +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 (
- "image/color"
- "math"
- "slices"
-)
-
-// MeshSampler 保存独立网格快照和空间索引,支持并发采样
-type MeshSampler struct {
- mesh MeshShading
- nodes []meshSampleNode
-}
-
-// meshSampleNode 保存连续三角形组的包围盒及右子树位置
-type meshSampleNode struct {
- minX, minY, maxX, maxY float64
- right int
-}
-
-// NewSampler 创建网格快照,后续修改原网格不影响采样结果
-// 返回: *MeshSampler 只读采样器
-func (m *MeshShading) NewSampler() *MeshSampler {
- s := &MeshSampler{}
- if m != nil {
- s.mesh = *m
- s.mesh.Triangles = slices.Clone(m.Triangles)
- if len(m.Triangles) > 8 {
- s.build(0, len(m.Triangles))
- }
- }
- return s
-}
-
-// At 采样快照中的预乘颜色,后绘制的三角形优先
-// 入参: x 横坐标, y 纵坐标
-// 返回: color.RGBA 预乘颜色
-func (s *MeshSampler) At(x, y float64) color.RGBA {
- if len(s.nodes) == 0 || !finite(x) || !finite(y) {
- return s.mesh.At(x, y)
- }
- if value, ok := s.sample(0, 0, len(s.mesh.Triangles), x, y); ok {
- return value
- }
- return s.mesh.Background
-}
-
-// build 按绘制顺序构建包围盒层级,不重排原三角形
-// 入参: first 起始三角形, end 结束位置
-// 返回: int 节点位置
-func (s *MeshSampler) build(first, end int) int {
- index := len(s.nodes)
- s.nodes = append(s.nodes, meshSampleNode{})
- if end-first <= 8 {
- node := meshSampleNode{minX: math.Inf(1), minY: math.Inf(1), maxX: math.Inf(-1), maxY: math.Inf(-1)}
- for _, triangle := range s.mesh.Triangles[first:end] {
- a, b, c := triangle[0].Point, triangle[1].Point, triangle[2].Point
- d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
- if !finite(d) {
- node = meshSampleNode{minX: math.Inf(-1), minY: math.Inf(-1), maxX: math.Inf(1), maxY: math.Inf(1)}
- break
- }
- for _, vertex := range triangle {
- node.minX, node.minY = math.Min(node.minX, vertex.Point.X), math.Min(node.minY, vertex.Point.Y)
- node.maxX, node.maxY = math.Max(node.maxX, vertex.Point.X), math.Max(node.maxY, vertex.Point.Y)
- }
- }
- if finite(node.minX) && finite(node.minY) && finite(node.maxX) && finite(node.maxY) {
- dx := 2e-12*(node.maxX-node.minX) + 1e-14*math.Max(math.Abs(node.minX), math.Abs(node.maxX))
- dy := 2e-12*(node.maxY-node.minY) + 1e-14*math.Max(math.Abs(node.minY), math.Abs(node.maxY))
- node.minX, node.maxX = math.Nextafter(node.minX-dx, math.Inf(-1)), math.Nextafter(node.maxX+dx, math.Inf(1))
- node.minY, node.maxY = math.Nextafter(node.minY-dy, math.Inf(-1)), math.Nextafter(node.maxY+dy, math.Inf(1))
- }
- s.nodes[index] = node
- return index
- }
- middle := first + (end-first)/2
- left := s.build(first, middle)
- right := s.build(middle, end)
- a, b := s.nodes[left], s.nodes[right]
- s.nodes[index] = meshSampleNode{minX: math.Min(a.minX, b.minX), minY: math.Min(a.minY, b.minY), maxX: math.Max(a.maxX, b.maxX), maxY: math.Max(a.maxY, b.maxY), right: right}
- return index
-}
-
-// sample 优先查询后绘制的区域,包围盒外跳过整组三角形
-// 入参: index 节点位置, first 起始三角形, end 结束位置, x 横坐标, y 纵坐标
-// 返回: color.RGBA 采样颜色, bool 是否命中
-func (s *MeshSampler) sample(index, first, end int, x, y float64) (color.RGBA, bool) {
- node := s.nodes[index]
- if x < node.minX || x > node.maxX || y < node.minY || y > node.maxY {
- return color.RGBA{}, false
- }
- if end-first <= 8 {
- for i := end - 1; i >= first; i-- {
- if value, ok := meshTriangleColor(&s.mesh.Triangles[i], x, y); ok {
- return value, true
- }
- }
- return color.RGBA{}, false
- }
- middle := first + (end-first)/2
- if value, ok := s.sample(node.right, middle, end, x, y); ok {
- return value, true
- }
- return s.sample(index+1, first, middle, x, y)
-}
diff --git a/ofdgo_paint.go b/ofdgo_paint.go
index 63fb69c..d790e2e 100644
--- a/ofdgo_paint.go
+++ b/ofdgo_paint.go
@@ -30,6 +30,12 @@ type PatternPaint struct {
Alpha *int
}
+// profileGradient 保存独立色标和只读配置,延伸及重复由几何采样器处理
+type profileGradient struct {
+ positions []float64
+ colors []MeshVertex
+}
+
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
// 返回: color.RGBA 标准颜色, error 颜色定义或配置错误
@@ -394,3 +400,70 @@ func colorToRGBA(c color.Color) color.RGBA {
r, g, b, a := c.RGBA()
return color.RGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
+
+// profileGradient 准备ICC渐变的源分量采样,不用转换后的端点近似非线性色彩变换
+// 入参: segments 原色标, alpha 外层透明度
+// 返回: func(float64) color.RGBA 单位区间采样器,无ICC时为nil, error 配置错误
+func (r *Renderer) profileGradient(segments []ShdSegment, alpha *int) (func(float64) color.RGBA, error) {
+ profiled := false
+ for _, segment := range segments {
+ if space := r.colorDefinition(segment.Color.ColorSpace); space != nil && space.Profile != "" {
+ profiled = true
+ break
+ }
+ }
+ if !profiled {
+ return nil, nil
+ }
+ gradient := &profileGradient{positions: gradientPositions(segments), colors: make([]MeshVertex, len(segments))}
+ for i, segment := range segments {
+ kind, values, err := r.colorComponents(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace)
+ if err != nil {
+ return nil, err
+ }
+ profile, err := r.Reader.colorProfile(r.colorDefinition(segment.Color.ColorSpace))
+ if err != nil {
+ return nil, err
+ }
+ opacity := 1.0
+ if value := mergeAlpha(segment.Color.Alpha, alpha); value != nil {
+ opacity = float64(clampColor(*value)) / 255
+ }
+ gradient.colors[i] = MeshVertex{Space: kind, Values: values, Alpha: opacity, profile: profile}
+ }
+ return gradient.At, nil
+}
+
+// At 在相同源空间内插值,不同空间先转换为sRGB,保留重复位置的右侧色标
+// 入参: t 单位区间位置
+// 返回: color.RGBA 预乘采样颜色
+func (g *profileGradient) At(t float64) color.RGBA {
+ if len(g.colors) == 0 || math.IsNaN(t) {
+ return color.RGBA{}
+ }
+ first := g.colors[0]
+ if t < g.positions[0] {
+ r, green, b := first.rgb(first.Values)
+ return profileColor([3]float64{r, green, b}, first.Alpha)
+ }
+ for i := 1; i < len(g.colors); i++ {
+ if t < g.positions[i] {
+ before, after := g.colors[i-1], g.colors[i]
+ x := (t - g.positions[i-1]) / (g.positions[i] - g.positions[i-1])
+ if before.sameSpace(after) {
+ var values [4]float64
+ for j := range values {
+ values[j] = before.Values[j]*(1-x) + after.Values[j]*x
+ }
+ r, green, b := before.rgb(values)
+ return profileColor([3]float64{r, green, b}, before.Alpha*(1-x)+after.Alpha*x)
+ }
+ r0, g0, b0 := before.rgb(before.Values)
+ r1, g1, b1 := after.rgb(after.Values)
+ return color.RGBA{meshByte(r0*before.Alpha*(1-x) + r1*after.Alpha*x), meshByte(g0*before.Alpha*(1-x) + g1*after.Alpha*x), meshByte(b0*before.Alpha*(1-x) + b1*after.Alpha*x), meshByte(before.Alpha*(1-x) + after.Alpha*x)}
+ }
+ }
+ last := g.colors[len(g.colors)-1]
+ r, green, b := last.rgb(last.Values)
+ return profileColor([3]float64{r, green, b}, last.Alpha)
+}
diff --git a/ofdgo_paint_mesh.go b/ofdgo_paint_mesh.go
index f2f64f7..ec83455 100644
--- a/ofdgo_paint_mesh.go
+++ b/ofdgo_paint_mesh.go
@@ -19,6 +19,7 @@ import (
"fmt"
"image/color"
"math"
+ "slices"
"strconv"
"strings"
)
@@ -61,6 +62,18 @@ type MeshShading struct {
Background color.RGBA
}
+// MeshSampler 保存独立网格快照和空间索引,支持并发采样
+type MeshSampler struct {
+ mesh MeshShading
+ nodes []meshSampleNode
+}
+
+// meshSampleNode 保存连续三角形组的包围盒及右子树位置
+type meshSampleNode struct {
+ minX, minY, maxX, maxY float64
+ right int
+}
+
// UnmarshalXML 解析高洛德渐变的控制点和背景色
// 入参: d XML解码器, start 起始节点
// 返回: error 解码错误
@@ -355,3 +368,90 @@ func meshRGB(space string, values [4]float64) (float64, float64, float64) {
// 入参: value 分量
// 返回: uint8 量化结果
func meshByte(value float64) uint8 { return uint8(math.Round(math.Max(0, math.Min(1, value)) * 255)) }
+
+// NewSampler 创建网格快照,后续修改原网格不影响采样结果
+// 返回: *MeshSampler 只读采样器
+func (m *MeshShading) NewSampler() *MeshSampler {
+ s := &MeshSampler{}
+ if m != nil {
+ s.mesh = *m
+ s.mesh.Triangles = slices.Clone(m.Triangles)
+ if len(m.Triangles) > 8 {
+ s.build(0, len(m.Triangles))
+ }
+ }
+ return s
+}
+
+// At 采样快照中的预乘颜色,后绘制的三角形优先
+// 入参: x 横坐标, y 纵坐标
+// 返回: color.RGBA 预乘颜色
+func (s *MeshSampler) At(x, y float64) color.RGBA {
+ if len(s.nodes) == 0 || !finite(x) || !finite(y) {
+ return s.mesh.At(x, y)
+ }
+ if value, ok := s.sample(0, 0, len(s.mesh.Triangles), x, y); ok {
+ return value
+ }
+ return s.mesh.Background
+}
+
+// build 按绘制顺序构建包围盒层级,不重排原三角形
+// 入参: first 起始三角形, end 结束位置
+// 返回: int 节点位置
+func (s *MeshSampler) build(first, end int) int {
+ index := len(s.nodes)
+ s.nodes = append(s.nodes, meshSampleNode{})
+ if end-first <= 8 {
+ node := meshSampleNode{minX: math.Inf(1), minY: math.Inf(1), maxX: math.Inf(-1), maxY: math.Inf(-1)}
+ for _, triangle := range s.mesh.Triangles[first:end] {
+ a, b, c := triangle[0].Point, triangle[1].Point, triangle[2].Point
+ d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
+ if !finite(d) {
+ node = meshSampleNode{minX: math.Inf(-1), minY: math.Inf(-1), maxX: math.Inf(1), maxY: math.Inf(1)}
+ break
+ }
+ for _, vertex := range triangle {
+ node.minX, node.minY = math.Min(node.minX, vertex.Point.X), math.Min(node.minY, vertex.Point.Y)
+ node.maxX, node.maxY = math.Max(node.maxX, vertex.Point.X), math.Max(node.maxY, vertex.Point.Y)
+ }
+ }
+ if finite(node.minX) && finite(node.minY) && finite(node.maxX) && finite(node.maxY) {
+ dx := 2e-12*(node.maxX-node.minX) + 1e-14*math.Max(math.Abs(node.minX), math.Abs(node.maxX))
+ dy := 2e-12*(node.maxY-node.minY) + 1e-14*math.Max(math.Abs(node.minY), math.Abs(node.maxY))
+ node.minX, node.maxX = math.Nextafter(node.minX-dx, math.Inf(-1)), math.Nextafter(node.maxX+dx, math.Inf(1))
+ node.minY, node.maxY = math.Nextafter(node.minY-dy, math.Inf(-1)), math.Nextafter(node.maxY+dy, math.Inf(1))
+ }
+ s.nodes[index] = node
+ return index
+ }
+ middle := first + (end-first)/2
+ left := s.build(first, middle)
+ right := s.build(middle, end)
+ a, b := s.nodes[left], s.nodes[right]
+ s.nodes[index] = meshSampleNode{minX: math.Min(a.minX, b.minX), minY: math.Min(a.minY, b.minY), maxX: math.Max(a.maxX, b.maxX), maxY: math.Max(a.maxY, b.maxY), right: right}
+ return index
+}
+
+// sample 优先查询后绘制的区域,包围盒外跳过整组三角形
+// 入参: index 节点位置, first 起始三角形, end 结束位置, x 横坐标, y 纵坐标
+// 返回: color.RGBA 采样颜色, bool 是否命中
+func (s *MeshSampler) sample(index, first, end int, x, y float64) (color.RGBA, bool) {
+ node := s.nodes[index]
+ if x < node.minX || x > node.maxX || y < node.minY || y > node.maxY {
+ return color.RGBA{}, false
+ }
+ if end-first <= 8 {
+ for i := end - 1; i >= first; i-- {
+ if value, ok := meshTriangleColor(&s.mesh.Triangles[i], x, y); ok {
+ return value, true
+ }
+ }
+ return color.RGBA{}, false
+ }
+ middle := first + (end-first)/2
+ if value, ok := s.sample(node.right, middle, end, x, y); ok {
+ return value, true
+ }
+ return s.sample(index+1, first, middle, x, y)
+}
diff --git a/ofdgo_paint_profile.go b/ofdgo_paint_profile.go
deleted file mode 100644
index 5ab4fff..0000000
--- a/ofdgo_paint_profile.go
+++ /dev/null
@@ -1,93 +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 (
- "image/color"
- "math"
-)
-
-// profileGradient 保存独立色标和只读配置,延伸及重复由几何采样器处理
-type profileGradient struct {
- positions []float64
- colors []MeshVertex
-}
-
-// profileGradient 准备ICC渐变的源分量采样,不用转换后的端点近似非线性色彩变换
-// 入参: segments 原色标, alpha 外层透明度
-// 返回: func(float64) color.RGBA 单位区间采样器,无ICC时为nil, error 配置错误
-func (r *Renderer) profileGradient(segments []ShdSegment, alpha *int) (func(float64) color.RGBA, error) {
- profiled := false
- for _, segment := range segments {
- if space := r.colorDefinition(segment.Color.ColorSpace); space != nil && space.Profile != "" {
- profiled = true
- break
- }
- }
- if !profiled {
- return nil, nil
- }
- gradient := &profileGradient{positions: gradientPositions(segments), colors: make([]MeshVertex, len(segments))}
- for i, segment := range segments {
- kind, values, err := r.colorComponents(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace)
- if err != nil {
- return nil, err
- }
- profile, err := r.Reader.colorProfile(r.colorDefinition(segment.Color.ColorSpace))
- if err != nil {
- return nil, err
- }
- opacity := 1.0
- if value := mergeAlpha(segment.Color.Alpha, alpha); value != nil {
- opacity = float64(clampColor(*value)) / 255
- }
- gradient.colors[i] = MeshVertex{Space: kind, Values: values, Alpha: opacity, profile: profile}
- }
- return gradient.At, nil
-}
-
-// At 在相同源空间内插值,不同空间先转换为sRGB,保留重复位置的右侧色标
-// 入参: t 单位区间位置
-// 返回: color.RGBA 预乘采样颜色
-func (g *profileGradient) At(t float64) color.RGBA {
- if len(g.colors) == 0 || math.IsNaN(t) {
- return color.RGBA{}
- }
- first := g.colors[0]
- if t < g.positions[0] {
- r, green, b := first.rgb(first.Values)
- return profileColor([3]float64{r, green, b}, first.Alpha)
- }
- for i := 1; i < len(g.colors); i++ {
- if t < g.positions[i] {
- before, after := g.colors[i-1], g.colors[i]
- x := (t - g.positions[i-1]) / (g.positions[i] - g.positions[i-1])
- if before.sameSpace(after) {
- var values [4]float64
- for j := range values {
- values[j] = before.Values[j]*(1-x) + after.Values[j]*x
- }
- r, green, b := before.rgb(values)
- return profileColor([3]float64{r, green, b}, before.Alpha*(1-x)+after.Alpha*x)
- }
- r0, g0, b0 := before.rgb(before.Values)
- r1, g1, b1 := after.rgb(after.Values)
- return color.RGBA{meshByte(r0*before.Alpha*(1-x) + r1*after.Alpha*x), meshByte(g0*before.Alpha*(1-x) + g1*after.Alpha*x), meshByte(b0*before.Alpha*(1-x) + b1*after.Alpha*x), meshByte(before.Alpha*(1-x) + after.Alpha*x)}
- }
- }
- last := g.colors[len(g.colors)-1]
- r, green, b := last.rgb(last.Values)
- return profileColor([3]float64{r, green, b}, last.Alpha)
-}
diff --git a/ofdgo_raster.go b/ofdgo_raster.go
index 67b0f24..9422f36 100644
--- a/ofdgo_raster.go
+++ b/ofdgo_raster.go
@@ -15,10 +15,13 @@
package ofdgo
import (
+ "context"
"fmt"
"image"
"image/color"
"math"
+
+ basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
)
// 光栅后端接收的路径指令
@@ -278,3 +281,104 @@ func (g *RasterGradient) colorAt(t float64) color.RGBA {
}
return g.Stops[len(g.Stops)-1].Color
}
+
+// Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素
+// 包含描边与指令裁剪,不应用页面边界,不排除图片透明像素或渐变透明区域
+// 入参: geometry 几何后端
+// 返回: Box 毫米坐标范围, error 几何或指令错误
+func (p *RasterPage) Bounds(geometry GeometryBackend) (Box, error) {
+ return p.BoundsContext(context.Background(), geometry)
+}
+
+// BoundsContext 按上下文度量只读绘制指令,取消或失败时不返回部分范围
+// 入参: ctx 取消上下文, geometry 几何后端
+// 返回: Box 毫米坐标范围, error 取消、几何或指令错误
+func (p *RasterPage) BoundsContext(ctx context.Context, geometry GeometryBackend) (Box, error) {
+ if err := ctx.Err(); err != nil {
+ return Box{}, err
+ }
+ if p == nil {
+ return Box{}, fmt.Errorf("invalid raster page")
+ }
+ if geometry == nil {
+ return Box{}, fmt.Errorf("raster bounds: %w", ErrBackendUnavailable)
+ }
+ var box Box
+ for index, command := range p.Commands {
+ if err := ctx.Err(); err != nil {
+ return Box{}, err
+ }
+ path, err := rasterCommandGeometry(geometry, command)
+ if err == nil {
+ var bounds Box
+ bounds, err = geometry.Bounds(path)
+ box = unionTextBox(box, bounds)
+ }
+ if err != nil {
+ return Box{}, fmt.Errorf("raster command %d bounds: %w", index, err)
+ }
+ }
+ if err := ctx.Err(); err != nil {
+ return Box{}, err
+ }
+ return box, nil
+}
+
+// rasterCommandGeometry 共用指令的页面变换、描边与裁剪,不修改路径或样式
+// 入参: geometry 几何后端, command 绘制指令
+// 返回: GeometryPath 页面轮廓, error 几何或指令错误
+func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (GeometryPath, error) {
+ if command.Clip != nil && len(command.Clip) == 0 {
+ return nil, nil
+ }
+ var path GeometryPath
+ if command.Image != nil {
+ bounds := command.Image.Bounds()
+ if bounds.Empty() {
+ return nil, nil
+ }
+ path = GeometryPath(basegeom.Rectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}))
+ } else {
+ if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 {
+ return nil, nil
+ }
+ for _, segment := range command.Path {
+ if segment.Verb > RasterClose {
+ return nil, fmt.Errorf("unsupported raster path verb %d", segment.Verb)
+ }
+ }
+ path = geometryFromRaster(command.Path)
+ }
+ matrix := MatrixFromValues([6]float64(command.Transform))
+ path, err := geometry.Transform(path, matrix)
+ if err != nil {
+ return nil, err
+ }
+ if command.Image == nil && command.Stroke != nil {
+ if err := basegeom.ValidateStroke(*command.Stroke); err != nil {
+ return nil, err
+ }
+ path, err = geometry.Stroke(path, *command.Stroke)
+ if err != nil {
+ return nil, err
+ }
+ } else if command.EvenOdd && command.Clip != nil {
+ path, err = geometry.Normalize(path, true)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if command.Clip != nil {
+ for _, segment := range command.Clip {
+ if segment.Verb > RasterClose {
+ return nil, fmt.Errorf("unsupported raster clip verb %d", segment.Verb)
+ }
+ }
+ clip := geometryFromRaster(command.Clip)
+ path, err = clipGeometry(geometry, path, &clip)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return path, nil
+}
diff --git a/ofdgo_raster_bounds.go b/ofdgo_raster_bounds.go
deleted file mode 100644
index 62600a8..0000000
--- a/ofdgo_raster_bounds.go
+++ /dev/null
@@ -1,122 +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"
- "fmt"
- basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
-)
-
-// Bounds 度量已编译指令的页面坐标范围,不重新定位文字或读取图片像素
-// 包含描边与指令裁剪,不应用页面边界,不排除图片透明像素或渐变透明区域
-// 入参: geometry 几何后端
-// 返回: Box 毫米坐标范围, error 几何或指令错误
-func (p *RasterPage) Bounds(geometry GeometryBackend) (Box, error) {
- return p.BoundsContext(context.Background(), geometry)
-}
-
-// BoundsContext 按上下文度量只读绘制指令,取消或失败时不返回部分范围
-// 入参: ctx 取消上下文, geometry 几何后端
-// 返回: Box 毫米坐标范围, error 取消、几何或指令错误
-func (p *RasterPage) BoundsContext(ctx context.Context, geometry GeometryBackend) (Box, error) {
- if err := ctx.Err(); err != nil {
- return Box{}, err
- }
- if p == nil {
- return Box{}, fmt.Errorf("invalid raster page")
- }
- if geometry == nil {
- return Box{}, fmt.Errorf("raster bounds: %w", ErrBackendUnavailable)
- }
- var box Box
- for index, command := range p.Commands {
- if err := ctx.Err(); err != nil {
- return Box{}, err
- }
- path, err := rasterCommandGeometry(geometry, command)
- if err == nil {
- var bounds Box
- bounds, err = geometry.Bounds(path)
- box = unionTextBox(box, bounds)
- }
- if err != nil {
- return Box{}, fmt.Errorf("raster command %d bounds: %w", index, err)
- }
- }
- if err := ctx.Err(); err != nil {
- return Box{}, err
- }
- return box, nil
-}
-
-// rasterCommandGeometry 共用指令的页面变换、描边与裁剪,不修改路径或样式
-// 入参: geometry 几何后端, command 绘制指令
-// 返回: GeometryPath 页面轮廓, error 几何或指令错误
-func rasterCommandGeometry(geometry GeometryBackend, command RasterCommand) (GeometryPath, error) {
- if command.Clip != nil && len(command.Clip) == 0 {
- return nil, nil
- }
- var path GeometryPath
- if command.Image != nil {
- bounds := command.Image.Bounds()
- if bounds.Empty() {
- return nil, nil
- }
- path = GeometryPath(basegeom.Rectangle(Box{X: float64(bounds.Min.X), Y: float64(bounds.Min.Y), W: float64(bounds.Dx()), H: float64(bounds.Dy())}))
- } else {
- if len(command.Path) == 0 || command.Paint.Gradient == nil && command.Paint.Color.A == 0 {
- return nil, nil
- }
- for _, segment := range command.Path {
- if segment.Verb > RasterClose {
- return nil, fmt.Errorf("unsupported raster path verb %d", segment.Verb)
- }
- }
- path = geometryFromRaster(command.Path)
- }
- matrix := MatrixFromValues([6]float64(command.Transform))
- path, err := geometry.Transform(path, matrix)
- if err != nil {
- return nil, err
- }
- if command.Image == nil && command.Stroke != nil {
- if err := basegeom.ValidateStroke(*command.Stroke); err != nil {
- return nil, err
- }
- path, err = geometry.Stroke(path, *command.Stroke)
- if err != nil {
- return nil, err
- }
- } else if command.EvenOdd && command.Clip != nil {
- path, err = geometry.Normalize(path, true)
- if err != nil {
- return nil, err
- }
- }
- if command.Clip != nil {
- for _, segment := range command.Clip {
- if segment.Verb > RasterClose {
- return nil, fmt.Errorf("unsupported raster clip verb %d", segment.Verb)
- }
- }
- clip := geometryFromRaster(command.Clip)
- path, err = clipGeometry(geometry, path, &clip)
- if err != nil {
- return nil, err
- }
- }
- return path, nil
-}
diff --git a/ofdgo_raster_clip.go b/ofdgo_raster_clip.go
index 965a932..1322119 100644
--- a/ofdgo_raster_clip.go
+++ b/ofdgo_raster_clip.go
@@ -23,6 +23,7 @@ import (
"math"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
+ "golang.org/x/image/draw"
)
// rasterClipKey 区分页面尺寸、分辨率和裁剪路径
@@ -114,3 +115,41 @@ func rasterClipMask(backend RasterBackend, page *RasterPage, path []RasterSegmen
cache.put(key, mask, len(mask.Pix)+256)
return mask, nil
}
+
+// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
+// 入参: m 局部到页面变换, height 目标像素高度
+// 返回: RasterMatrix 局部到像素变换
+func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
+ scale := p.DPI / 25.4
+ for i := range m {
+ m[i] *= scale
+ }
+ m[5] += float64(height) - p.Height*scale
+ return m
+}
+
+// drawRasterImageCommand 使用当前后端生成裁剪蒙版,共用高质量图像采样
+// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令, cache 蒙版缓存
+// 返回: error 裁剪绘制错误
+func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Image, src image.Image, command RasterCommand, cache *renderCache[[32]byte, *image.Alpha]) error {
+ m := page.PixelTransform(command.Transform, dst.Bounds().Dy())
+ if command.Clip == nil {
+ baseimage.DrawPixels(dst, src, m, draw.Over)
+ return nil
+ }
+ mask, err := rasterClipMask(backend, page, command.Clip, cache)
+ if err != nil {
+ return err
+ }
+ if mask.Rect.Empty() {
+ return nil
+ }
+ bounds := mask.Rect.Intersect(dst.Bounds()).Intersect(baseimage.TransformBounds(src.Bounds(), m, dst.Bounds()))
+ if bounds.Empty() {
+ return nil
+ }
+ layer := image.NewRGBA64(bounds)
+ baseimage.DrawPixels(layer, baseimage.PixelSource(src), m, draw.Src)
+ draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
+ return nil
+}
diff --git a/ofdgo_raster_image.go b/ofdgo_raster_image.go
deleted file mode 100644
index 204f495..0000000
--- a/ofdgo_raster_image.go
+++ /dev/null
@@ -1,60 +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 (
- "image"
-
- baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
- "golang.org/x/image/draw"
-)
-
-// PixelTransform 将页面毫米变换转换为像素变换,保留尺寸取整后的原点位置
-// 入参: m 局部到页面变换, height 目标像素高度
-// 返回: RasterMatrix 局部到像素变换
-func (p *RasterPage) PixelTransform(m RasterMatrix, height int) RasterMatrix {
- scale := p.DPI / 25.4
- for i := range m {
- m[i] *= scale
- }
- m[5] += float64(height) - p.Height*scale
- return m
-}
-
-// drawRasterImageCommand 使用当前后端生成裁剪蒙版,共用高质量图像采样
-// 入参: backend 当前后端, page 页面, dst 目标图像, src 原始图像, command 图像指令, cache 蒙版缓存
-// 返回: error 裁剪绘制错误
-func drawRasterImageCommand(backend RasterBackend, page *RasterPage, dst draw.Image, src image.Image, command RasterCommand, cache *renderCache[[32]byte, *image.Alpha]) error {
- m := page.PixelTransform(command.Transform, dst.Bounds().Dy())
- if command.Clip == nil {
- baseimage.DrawPixels(dst, src, m, draw.Over)
- return nil
- }
- mask, err := rasterClipMask(backend, page, command.Clip, cache)
- if err != nil {
- return err
- }
- if mask.Rect.Empty() {
- return nil
- }
- bounds := mask.Rect.Intersect(dst.Bounds()).Intersect(baseimage.TransformBounds(src.Bounds(), m, dst.Bounds()))
- if bounds.Empty() {
- return nil
- }
- layer := image.NewRGBA64(bounds)
- baseimage.DrawPixels(layer, baseimage.PixelSource(src), m, draw.Src)
- draw.DrawMask(dst, bounds, layer, bounds.Min, mask, bounds.Min, draw.Over)
- return nil
-}
diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go
index 0c89d35..8ed1e40 100644
--- a/ofdgo_render_compile.go
+++ b/ofdgo_render_compile.go
@@ -16,11 +16,16 @@ package ofdgo
import (
"fmt"
- basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
+ "image/color"
"math"
"strconv"
+
+ basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
)
+// OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置
+type OFDCompiler struct{}
+
// semanticCompiler 将公共页面语义编译为路径和原始图片,绘制、搜索和度量共享定位
type semanticCompiler struct {
renderer *Renderer
@@ -498,3 +503,78 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
}
return nil
}
+
+// Name 返回页面编译器标识
+// 返回: string 编译器标识
+func (OFDCompiler) Name() string { return "ofd" }
+
+// CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面
+// 入参: r 渲染器, page 页面
+// 返回: *RasterPage 绘制页面, error 编译错误
+func (OFDCompiler) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) {
+ box, err := r.GetPageBox(page)
+ if err != nil {
+ return nil, err
+ }
+ c, err := newSemanticCompiler(r)
+ if err != nil {
+ return nil, err
+ }
+ c.page = &RasterPage{Width: box.W, Height: box.H, DPI: r.DPI}
+ if _, _, err := c.page.PixelSize(); err != nil {
+ return nil, err
+ }
+ if !r.TransparentBackground {
+ if err := c.fill(GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H})), Paint{Kind: PaintSolid, Color: color.RGBA{255, 255, 255, 255}}, false, nil, IdentityMatrix, IdentityMatrix); err != nil {
+ return nil, err
+ }
+ }
+ if err := r.WalkPage(page, c); err != nil {
+ return nil, err
+ }
+ if r.OnPageText != nil {
+ r.OnPageText(page, &c.text)
+ }
+ return c.page, nil
+}
+
+// PageText 提取与页面绘制相同定位的原文,不读取图片像素
+// 入参: r 渲染器, page 页面
+// 返回: *PageText 页面文字, error 定位错误
+func (OFDCompiler) PageText(r *Renderer, page *PageContent) (*PageText, error) {
+ if _, err := r.GetPageBox(page); err != nil {
+ return nil, err
+ }
+ c, err := newSemanticCompiler(r)
+ if err != nil {
+ return nil, err
+ }
+ c.textOnly = true
+ if err := r.WalkPage(page, c); err != nil {
+ return nil, err
+ }
+ return &c.text, nil
+}
+
+// MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素
+// 入参: r 渲染器, object 对象, options 度量上下文
+// 返回: ObjectMeasurement 对象范围与轮廓, error 度量错误
+func (OFDCompiler) MeasureObject(r *Renderer, object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) {
+ c, err := newSemanticCompiler(r)
+ if err != nil {
+ return ObjectMeasurement{}, err
+ }
+ boundary, ctm := editorGeometry(object)
+ if _, err := creationBox(boundary); err != nil {
+ return ObjectMeasurement{}, err
+ }
+ if ctm != "" {
+ if _, err := creationNumbers(ctm, 6); err != nil {
+ return ObjectMeasurement{}, err
+ }
+ }
+ c.measure = true
+ c.contours = options.Contours
+ err = r.WalkObject(&object, RenderState{Defaults: options.Defaults, Parent: options.Parent, BoundaryInCTM: options.BoundaryInCTM, Clip: options.Clip}, c)
+ return c.measurement, err
+}
diff --git a/ofdgo_render_preview.go b/ofdgo_render_preview.go
deleted file mode 100644
index 77355a0..0000000
--- a/ofdgo_render_preview.go
+++ /dev/null
@@ -1,128 +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 (
- "bytes"
- "context"
- "crypto/sha256"
- "fmt"
- "io"
-)
-
-// svgNamespace 表示SVG预览文档命名空间
-const svgNamespace = "http://www.w3.org/2000/svg"
-
-// PreviewOptions 指定页面显示方式,光栅预览不提供SVG对象分组
-type PreviewOptions struct {
- Raster bool
- Objects bool
-}
-
-// RenderPreview 生成统一SVG容器及独立资源,原生程序与WebUI使用相同绘制入口
-// 入参: page 页面内容, writer 输出流, options 显示方式
-// 返回: SVGResources 需由调用方映射地址的资源, error 错误信息
-func (r *Renderer) RenderPreview(page *PageContent, writer io.Writer, options PreviewOptions) (SVGResources, error) {
- if !options.Raster {
- if options.Objects {
- return r.RenderToSVGWithObjects(page, writer)
- }
- return r.RenderToSVGWithResources(page, writer)
- }
- if options.Objects {
- return SVGResources{}, fmt.Errorf("raster preview does not provide SVG object groups")
- }
- box, err := r.GetPageBox(page)
- if err != nil {
- return SVGResources{}, err
- }
- var pixels bytes.Buffer
- if err := r.RenderToPNG(page, &pixels); err != nil {
- return SVGResources{}, err
- }
- name := fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(pixels.Bytes()))
- _, err = fmt.Fprintf(writer, ``, box.W, box.H, box.W, box.H, box.W, box.H, name)
- if err != nil {
- return SVGResources{}, err
- }
- return SVGResources{Images: []SVGImage{{Name: name, MIME: "image/png", Data: pixels.Bytes()}}}, nil
-}
-
-// RenderFontPreview 将指定字体的前32个可见字形绘制为独立SVG,不依赖字符映射或浏览器字体
-// 排除缺字字形,外部字体须精确匹配,不修改原始资源或使用无关回退字体
-// 入参: ctx 取消上下文, id 字体资源标识, writer 输出流
-// 返回: error 字体、轮廓、取消或写入错误
-func (r *Renderer) RenderFontPreview(ctx context.Context, id string, writer io.Writer) error {
- if err := ctx.Err(); err != nil {
- return err
- }
- view := *r
- view.pageContext = ctx
- resolved, err := view.ResolveFont(id, true)
- if err != nil {
- return err
- }
- font := &PreparedFont{ResolvedFont: resolved}
- if definition := view.Reader.fontDefinition(id); definition != nil && definition.FontFile != "" {
- font, err = view.PrepareFont(id)
- if err != nil {
- return err
- }
- }
- metrics, err := view.preparedMetrics(font)
- if err != nil {
- return err
- }
- outlines, ok := metrics.(FontOutlines)
- if !ok {
- return fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
- }
- var paths bytes.Buffer
- count := 0
- for glyph := uint16(1); glyph < metrics.NumGlyphs() && count < 32; glyph++ {
- if err := ctx.Err(); err != nil {
- return err
- }
- outline, err := outlines.GlyphOutline(glyph, 24)
- if err != nil {
- return err
- }
- box, err := outline.Bounds()
- if err != nil {
- return err
- }
- if box.W <= 0 || box.H <= 0 {
- continue
- }
- path, err := outline.SVG()
- if err != nil {
- return err
- }
- scale := min(1.0, 28/box.W, 32/box.H)
- x := float64(count%8)*36 + (36-box.W*scale)/2 - box.X*scale
- y := float64(count/8)*40 + (40-box.H*scale)/2 - box.Y*scale
- fmt.Fprintf(&paths, ``, x, y, scale, path)
- count++
- }
- if err := ctx.Err(); err != nil {
- return err
- }
- if count == 0 {
- return fmt.Errorf("font has no visible glyphs")
- }
- width, height := min(count, 8)*36, (count+7)/8*40
- _, err = fmt.Fprintf(writer, ``, width, height, width, height, paths.Bytes())
- return err
-}
diff --git a/ofdgo_render_svg.go b/ofdgo_render_svg.go
index 8bfa701..a87c641 100644
--- a/ofdgo_render_svg.go
+++ b/ofdgo_render_svg.go
@@ -15,10 +15,16 @@
package ofdgo
import (
+ "bytes"
+ "context"
+ "crypto/sha256"
"fmt"
"io"
)
+// svgNamespace 表示SVG预览文档命名空间
+const svgNamespace = "http://www.w3.org/2000/svg"
+
// SVGFont SVG引用的字体资源,Name为CSS字体族标识,Data为只读字体数据
type SVGFont struct {
Name string
@@ -40,6 +46,12 @@ type SVGResources struct {
Images []SVGImage
}
+// PreviewOptions 指定页面显示方式,光栅预览不提供SVG对象分组
+type PreviewOptions struct {
+ Raster bool
+ Objects bool
+}
+
// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体
// 入参: page 页面内容, writer 输出流
// 返回: []SVGFont 字体资源,可按Name复用并按Weight、Style注册,error 错误信息
@@ -76,3 +88,99 @@ func (r *Renderer) renderSVG(page *PageContent, writer io.Writer, mode SVGMode)
writer = convertWriter{ctx: r.outputContext(), writer: writer, count: &written}
return r.backends.SVG.RenderSVG(r, page, writer, mode)
}
+
+// RenderPreview 生成统一SVG容器及独立资源,原生程序与WebUI使用相同绘制入口
+// 入参: page 页面内容, writer 输出流, options 显示方式
+// 返回: SVGResources 需由调用方映射地址的资源, error 错误信息
+func (r *Renderer) RenderPreview(page *PageContent, writer io.Writer, options PreviewOptions) (SVGResources, error) {
+ if !options.Raster {
+ if options.Objects {
+ return r.RenderToSVGWithObjects(page, writer)
+ }
+ return r.RenderToSVGWithResources(page, writer)
+ }
+ if options.Objects {
+ return SVGResources{}, fmt.Errorf("raster preview does not provide SVG object groups")
+ }
+ box, err := r.GetPageBox(page)
+ if err != nil {
+ return SVGResources{}, err
+ }
+ var pixels bytes.Buffer
+ if err := r.RenderToPNG(page, &pixels); err != nil {
+ return SVGResources{}, err
+ }
+ name := fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(pixels.Bytes()))
+ _, err = fmt.Fprintf(writer, ``, box.W, box.H, box.W, box.H, box.W, box.H, name)
+ if err != nil {
+ return SVGResources{}, err
+ }
+ return SVGResources{Images: []SVGImage{{Name: name, MIME: "image/png", Data: pixels.Bytes()}}}, nil
+}
+
+// RenderFontPreview 将指定字体的前32个可见字形绘制为独立SVG,不依赖字符映射或浏览器字体
+// 排除缺字字形,外部字体须精确匹配,不修改原始资源或使用无关回退字体
+// 入参: ctx 取消上下文, id 字体资源标识, writer 输出流
+// 返回: error 字体、轮廓、取消或写入错误
+func (r *Renderer) RenderFontPreview(ctx context.Context, id string, writer io.Writer) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ view := *r
+ view.pageContext = ctx
+ resolved, err := view.ResolveFont(id, true)
+ if err != nil {
+ return err
+ }
+ font := &PreparedFont{ResolvedFont: resolved}
+ if definition := view.Reader.fontDefinition(id); definition != nil && definition.FontFile != "" {
+ font, err = view.PrepareFont(id)
+ if err != nil {
+ return err
+ }
+ }
+ metrics, err := view.preparedMetrics(font)
+ if err != nil {
+ return err
+ }
+ outlines, ok := metrics.(FontOutlines)
+ if !ok {
+ return fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
+ }
+ var paths bytes.Buffer
+ count := 0
+ for glyph := uint16(1); glyph < metrics.NumGlyphs() && count < 32; glyph++ {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ outline, err := outlines.GlyphOutline(glyph, 24)
+ if err != nil {
+ return err
+ }
+ box, err := outline.Bounds()
+ if err != nil {
+ return err
+ }
+ if box.W <= 0 || box.H <= 0 {
+ continue
+ }
+ path, err := outline.SVG()
+ if err != nil {
+ return err
+ }
+ scale := min(1.0, 28/box.W, 32/box.H)
+ x := float64(count%8)*36 + (36-box.W*scale)/2 - box.X*scale
+ y := float64(count/8)*40 + (40-box.H*scale)/2 - box.Y*scale
+ fmt.Fprintf(&paths, ``, x, y, scale, path)
+ count++
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ if count == 0 {
+ return fmt.Errorf("font has no visible glyphs")
+ }
+ width, height := min(count, 8)*36, (count+7)/8*40
+ _, err = fmt.Fprintf(writer, ``, width, height, width, height, paths.Bytes())
+ return err
+}
diff --git a/ofdgo_renderer_compile.go b/ofdgo_renderer_compile.go
deleted file mode 100644
index 7be5bf9..0000000
--- a/ofdgo_renderer_compile.go
+++ /dev/null
@@ -1,98 +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 (
- basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
- "image/color"
-)
-
-// OFDCompiler 使用库自有页面语义,字体和几何操作交给当前后端配置
-type OFDCompiler struct{}
-
-// Name 返回页面编译器标识
-// 返回: string 编译器标识
-func (OFDCompiler) Name() string { return "ofd" }
-
-// CompilePage 使用公共OFD语义和配置的字形、几何能力编译页面
-// 入参: r 渲染器, page 页面
-// 返回: *RasterPage 绘制页面, error 编译错误
-func (OFDCompiler) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) {
- box, err := r.GetPageBox(page)
- if err != nil {
- return nil, err
- }
- c, err := newSemanticCompiler(r)
- if err != nil {
- return nil, err
- }
- c.page = &RasterPage{Width: box.W, Height: box.H, DPI: r.DPI}
- if _, _, err := c.page.PixelSize(); err != nil {
- return nil, err
- }
- if !r.TransparentBackground {
- if err := c.fill(GeometryPath(basegeom.Rectangle(Box{W: box.W, H: box.H})), Paint{Kind: PaintSolid, Color: color.RGBA{255, 255, 255, 255}}, false, nil, IdentityMatrix, IdentityMatrix); err != nil {
- return nil, err
- }
- }
- if err := r.WalkPage(page, c); err != nil {
- return nil, err
- }
- if r.OnPageText != nil {
- r.OnPageText(page, &c.text)
- }
- return c.page, nil
-}
-
-// PageText 提取与页面绘制相同定位的原文,不读取图片像素
-// 入参: r 渲染器, page 页面
-// 返回: *PageText 页面文字, error 定位错误
-func (OFDCompiler) PageText(r *Renderer, page *PageContent) (*PageText, error) {
- if _, err := r.GetPageBox(page); err != nil {
- return nil, err
- }
- c, err := newSemanticCompiler(r)
- if err != nil {
- return nil, err
- }
- c.textOnly = true
- if err := r.WalkPage(page, c); err != nil {
- return nil, err
- }
- return &c.text, nil
-}
-
-// MeasureObject 复用文字和图形编译规则收集范围,不分配页面像素
-// 入参: r 渲染器, object 对象, options 度量上下文
-// 返回: ObjectMeasurement 对象范围与轮廓, error 度量错误
-func (OFDCompiler) MeasureObject(r *Renderer, object GraphicObject, options MeasureOptions) (ObjectMeasurement, error) {
- c, err := newSemanticCompiler(r)
- if err != nil {
- return ObjectMeasurement{}, err
- }
- boundary, ctm := editorGeometry(object)
- if _, err := creationBox(boundary); err != nil {
- return ObjectMeasurement{}, err
- }
- if ctm != "" {
- if _, err := creationNumbers(ctm, 6); err != nil {
- return ObjectMeasurement{}, err
- }
- }
- c.measure = true
- c.contours = options.Contours
- err = r.WalkObject(&object, RenderState{Defaults: options.Defaults, Parent: options.Parent, BoundaryInCTM: options.BoundaryInCTM, Clip: options.Clip}, c)
- return c.measurement, err
-}