perf(优化提升): 提升双端性能并优化内存占用
build.yaml / build (push) Successful in 46s

This commit is contained in:
xiaoqidun committed 2026-10-19 23:17:39 +08:00
1 parent eb20f66ffb
commit 075797402d
41 files changed
+626 -118

No files matched your search

+9 -2
View File
@@ -419,6 +419,7 @@ type outputContext struct {
// businessEngine 独立文档、编辑、剪贴板及操作上下文 // businessEngine 独立文档、编辑、剪贴板及操作上下文
type businessEngine struct { type businessEngine struct {
resources *ofdgo.Resources
currentSession *Session currentSession *Session
currentEditor *ofdgo.Editor currentEditor *ofdgo.Editor
saveSnapshot *ofdgo.Editor saveSnapshot *ofdgo.Editor
@@ -1396,7 +1397,7 @@ func (e *businessEngine) convertPDFDocument(args []platformValue) (any, error) {
return nil, err return nil, err
} }
var output bytes.Buffer var output bytes.Buffer
options := ofdgo.PDFImportOptions{OnProgress: e.editorOperationProgress(args[4]), PasswordUTF8: true} options := ofdgo.PDFImportOptions{Resources: e.resources, OnProgress: e.editorOperationProgress(args[4]), PasswordUTF8: true}
if !args[2].IsNull() && !args[2].IsUndefined() { if !args[2].IsNull() && !args[2].IsUndefined() {
options.Password, err = e.bytesFromJS(args[2]) options.Password, err = e.bytesFromJS(args[2])
if err != nil { if err != nil {
@@ -1450,6 +1451,7 @@ func (e *businessEngine) convertFile(args []platformValue) (any, error) {
return nil, err return nil, err
} }
options := ofdgo.ConvertOptions{ options := ofdgo.ConvertOptions{
Resources: e.resources,
Format: settings.Get("format").String(), PageRange: settings.Get("pages").String(), Format: settings.Get("format").String(), PageRange: settings.Get("pages").String(),
Pool: e.conversionPool, Pool: e.conversionPool,
SkipUnchanged: true, SkipUnchanged: true,
@@ -1608,7 +1610,7 @@ func (e *businessEngine) openDocument(args []platformValue) (any, error) {
return nil, err return nil, err
} }
renderAnnotations := args[2].Bool() renderAnnotations := args[2].Bool()
options := OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations} options := OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations, ReaderOptions: []ofdgo.ReaderOption{ofdgo.WithResources(e.resources)}}
if len(args) > 3 && !args[3].IsNull() && !args[3].IsUndefined() { if len(args) > 3 && !args[3].IsNull() && !args[3].IsUndefined() {
credentials, err := e.credentialsFromJS(args[3]) credentials, err := e.credentialsFromJS(args[3])
if err != nil { if err != nil {
@@ -4047,6 +4049,7 @@ func (e *businessEngine) previewText(args []platformValue) (any, error) {
return nil, err return nil, err
} }
editor = ofdgo.NewEditor() editor = ofdgo.NewEditor()
editor.SetResources(e.resources)
fontID, err = editor.AddFont(FontFile{Data: data}, 0) fontID, err = editor.AddFont(FontFile{Data: data}, 0)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -5568,6 +5571,7 @@ func (e *businessEngine) loadImport(args []platformValue) (any, error) {
} }
}() }()
var options []ofdgo.ReaderOption var options []ofdgo.ReaderOption
options = append(options, ofdgo.WithResources(e.resources))
if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() { if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() {
credentials, err := e.credentialsFromJS(args[1]) credentials, err := e.credentialsFromJS(args[1])
if err != nil { if err != nil {
@@ -5685,6 +5689,7 @@ func (e *businessEngine) importImages(args []platformValue) (any, error) {
editor := e.currentEditor editor := e.currentEditor
if at < 0 { if at < 0 {
editor = ofdgo.NewEditor() editor = ofdgo.NewEditor()
editor.SetResources(e.resources)
editor.Info.Title = args[3].String() editor.Info.Title = args[3].String()
} else if editor == nil { } else if editor == nil {
return nil, fmt.Errorf("document is not editable") return nil, fmt.Errorf("document is not editable")
@@ -5721,6 +5726,7 @@ func (e *businessEngine) importImages(args []platformValue) (any, error) {
func (e *businessEngine) createDocument(args []platformValue) (any, error) { func (e *businessEngine) createDocument(args []platformValue) (any, error) {
e.clearImport() e.clearImport()
editor := ofdgo.NewEditor() editor := ofdgo.NewEditor()
editor.SetResources(e.resources)
editor.Info.Title = args[0].String() editor.Info.Title = args[0].String()
if _, err := editor.AddPage(args[1].Float(), args[2].Float()); err != nil { if _, err := editor.AddPage(args[1].Float(), args[2].Float()); err != nil {
return nil, err return nil, err
@@ -7124,6 +7130,7 @@ func (e *businessEngine) previewSignatureSeal(args []platformValue) (any, error)
pages = append(pages, map[string]any{"index": index, "x": box.X, "y": box.Y, "width": box.W, "height": box.H}) pages = append(pages, map[string]any{"index": index, "x": box.X, "y": box.Y, "width": box.W, "height": box.H})
} }
editor := ofdgo.NewEditor() editor := ofdgo.NewEditor()
editor.SetResources(e.resources)
if _, err := editor.AddPage(seal.Width, seal.Height); err != nil { if _, err := editor.AddPage(seal.Width, seal.Height); err != nil {
return nil, err return nil, err
} }
+10
View File
@@ -70,6 +70,8 @@ type NativeOutputTarget struct {
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭 // hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
type NativeEngine struct { type NativeEngine struct {
businessEngine businessEngine
// Resources 协调会话的计算与缓存,需在首次Call前配置
Resources *ofdgo.Resources
// ConversionPool 协调不同会话的批量转换资源 // ConversionPool 协调不同会话的批量转换资源
ConversionPool *ofdgo.ConversionPool ConversionPool *ofdgo.ConversionPool
// RenderParallelism 限制同时渲染的页面数,零值使用可用处理器数 // RenderParallelism 限制同时渲染的页面数,零值使用可用处理器数
@@ -152,6 +154,7 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
e.renderSlots = make(chan struct{}, e.renderParallelism()) e.renderSlots = make(chan struct{}, e.renderParallelism())
e.resolveInput = e.input e.resolveInput = e.input
e.conversionPool = e.ConversionPool e.conversionPool = e.ConversionPool
e.resources = e.Resources
}) })
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
@@ -163,6 +166,13 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
case <-ctx.Done(): case <-ctx.Done():
return nativeFailure(ctx.Err()) return nativeFailure(ctx.Err())
} }
if e.resources != nil {
release, err := e.resources.Acquire(ctx, 64<<20)
if err != nil {
return nativeFailure(err)
}
defer release()
}
} }
select { select {
case e.gate <- struct{}{}: case e.gate <- struct{}{}:
+2 -1
View File
@@ -39,7 +39,8 @@ type platformValue = js.Value
// RunWASM 注册浏览器WASM接口并阻塞运行 // RunWASM 注册浏览器WASM接口并阻塞运行
func RunWASM() { func RunWASM() {
e := &businessEngine{} resources, _ := ofdgo.NewResources(ofdgo.ResourceOptions{Parallelism: 1, MemoryLimit: 64 << 20, CacheBytes: 48 << 20, SnapshotBytes: 32 << 20})
e := &businessEngine{resources: resources}
asynchronous := map[string]bool{ asynchronous := map[string]bool{
"ofdgoDetectFormat": true, "ofdgoDetectFormat": true,
"ofdgoExportVersion": true, "ofdgoExportVersion": true,
+12 -3
View File
@@ -51,6 +51,7 @@ type Host struct {
engines map[string]*webui.NativeEngine engines map[string]*webui.NativeEngine
engineStops map[string]func() bool engineStops map[string]func() bool
conversionPool *ofdgo.ConversionPool conversionPool *ofdgo.ConversionPool
resources *ofdgo.Resources
cancels map[string]context.CancelFunc cancels map[string]context.CancelFunc
batchResults map[string]any batchResults map[string]any
fontOnce sync.Once fontOnce sync.Once
@@ -79,11 +80,16 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
return err return err
} }
h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)} h := &Host{files: files, engines: make(map[string]*webui.NativeEngine), cancels: make(map[string]context.CancelFunc), fontFiles: make(map[string]string), pendingPaths: append([]string(nil), paths...)}
h.conversionPool, err = ofdgo.NewConversionPool(ofdgo.ConversionPoolOptions{AvailableMemory: conversionMemory}) cacheBytes := int64(256 << 20)
if memory := conversionMemory; memory != nil {
cacheBytes = max(32<<20, memory()/8)
}
h.resources, err = ofdgo.NewResources(ofdgo.ResourceOptions{CacheBytes: cacheBytes, AvailableMemory: conversionMemory})
if err != nil { if err != nil {
files.close() files.close()
return err return err
} }
h.conversionPool = h.resources.ConversionPool()
defer h.close() defer h.close()
bindings := make(map[string]guigo.HandlerFunc) bindings := make(map[string]guigo.HandlerFunc)
for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} { for _, name := range []string{"engine.open", "engine.close", "engine.call", "engine.start", "engine.cancel", "engine.finish", "batch", "open", "save", "directory", "file", "release", "child", "remove", "write.begin", "write.data", "write.commit", "write.abort", "fonts", "ready", "title"} {
@@ -271,11 +277,11 @@ func (h *Host) call(ctx context.Context, name string, value any) (any, error) {
parallelism, cacheBytes := runtime.GOMAXPROCS(0), int64(128<<20) parallelism, cacheBytes := runtime.GOMAXPROCS(0), int64(128<<20)
if memory := conversionMemory; memory != nil { if memory := conversionMemory; memory != nil {
available := memory() available := memory()
parallelism = max(1, min(parallelism, int(available/(256<<20)))) cacheBytes = max(32<<20, available/32)
cacheBytes = max(32<<20, min(256<<20, available/16))
} }
engine.RenderParallelism = parallelism engine.RenderParallelism = parallelism
engine.ConversionPool = h.conversionPool engine.ConversionPool = h.conversionPool
engine.Resources = h.resources
engine.OpenWriter = h.openWriter engine.OpenWriter = h.openWriter
engine.ReserveOutput = h.reserveOutput engine.ReserveOutput = h.reserveOutput
engine.CleanupOutput = h.cleanupOutput engine.CleanupOutput = h.cleanupOutput
@@ -798,6 +804,9 @@ func (h *Host) close() {
for _, result := range results { for _, result := range results {
h.discard(result) h.discard(result)
} }
if h.resources != nil {
h.resources.Clear()
}
_ = h.files.close() _ = h.files.close()
} }
+4 -1
View File
@@ -290,6 +290,9 @@ func NewReader(input *Input, options ...ReaderOption) (*Reader, error) {
// 入参: reader 阅读器, opts 渲染选项 // 入参: reader 阅读器, opts 渲染选项
// 返回: *Renderer 渲染器实例 // 返回: *Renderer 渲染器实例
func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
if reader.resources == nil {
reader.resources, _ = NewResources(ResourceOptions{})
}
r := &Renderer{ r := &Renderer{
Reader: reader, Reader: reader,
DPI: 300.0, DPI: 300.0,
@@ -297,7 +300,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache, DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache, CompositeGraphicUnits: reader.compositeGraphicUnitCache,
templatePageCache: make(map[string]*PageContent), templatePageCache: make(map[string]*PageContent),
imageCache: &renderCache[string, image.Image]{limit: imageCacheLimit}, imageCache: resourceCache[string, image.Image](reader.resources, imageCacheLimit),
backends: defaultRenderBackends(), backends: defaultRenderBackends(),
backendStates: make(map[any]any), backendStates: make(map[any]any),
} }
+2
View File
@@ -108,6 +108,7 @@ func (e *Editor) WriteToWithOptions(ctx context.Context, writer io.Writer, optio
// 入参: ctx 取消上下文, writer 输出流, indexes 零基页码, options 输出配置 // 入参: ctx 取消上下文, writer 输出流, indexes 零基页码, options 输出配置
// 返回: int64 已写入字节数, error 错误信息 // 返回: int64 已写入字节数, error 错误信息
func (e *Editor) WritePagesTo(ctx context.Context, writer io.Writer, indexes []int, options WriteOptions) (int64, error) { func (e *Editor) WritePagesTo(ctx context.Context, writer io.Writer, indexes []int, options WriteOptions) (int64, error) {
ctx = e.runtimeResources.context(ctx)
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return 0, err return 0, err
} }
@@ -125,6 +126,7 @@ func (e *Editor) WritePagesTo(ctx context.Context, writer io.Writer, indexes []i
// 入参: ctx 取消上下文, options 输出配置 // 入参: ctx 取消上下文, options 输出配置
// 返回: *Editor 输出快照, error 配置或资源错误 // 返回: *Editor 输出快照, error 配置或资源错误
func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Editor, error) { func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Editor, error) {
ctx = e.runtimeResources.context(ctx)
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
+9 -11
View File
@@ -20,16 +20,12 @@ import (
"crypto/sha256" "crypto/sha256"
"image" "image"
"io" "io"
"strconv"
"sync" "sync"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// outputImageBatchLimit 限制并行任务持有的原始编码,超限图片单独处理 // outputImageWorkingSetLimit 指定未提供任务预算时的图像成本估算上限
const outputImageBatchLimit = 16 << 20
// outputImageWorkingSetLimit 限制64位并行任务的像素工作量总估算,32位使用64MiB,超限图片单独处理
const outputImageWorkingSetLimit = 256 << 20 const outputImageWorkingSetLimit = 256 << 20
// outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调 // outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调
@@ -82,20 +78,22 @@ func (e *Editor) outputImageOptions(key string) (CompressionOptions, image.Point
// 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 候选缓存, sources 原始资源摘要,可为空 // 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 候选缓存, sources 原始资源摘要,可为空
// 返回: error 读取、进度或取消错误 // 返回: error 读取、进度或取消错误
func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, error), results map[outputImageKey][]byte, sources map[string]outputImageSource) error { func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, error), results map[outputImageKey][]byte, sources map[string]outputImageSource) error {
workers := conversionWorkers(e.output.ctx, 4) workers := conversionWorkers(e.output.ctx)
if workers < 2 || len(keys) < 2 { if workers < 2 || len(keys) < 2 {
return nil return nil
} }
limit, encodedLimit := outputImageWorkingSetLimit, outputImageBatchLimit limit := int(min(int64(workers)*(64<<20), int64(int(^uint(0)>>1)-1)))
encodedLimit := limit
if pool, _ := e.output.ctx.Value(conversionPoolKey{}).(*ConversionPool); pool != nil { if pool, _ := e.output.ctx.Value(conversionPoolKey{}).(*ConversionPool); pool != nil {
if available := pool.memoryLimit(); available > 0 { available := pool.memoryLimit()
if available == 0 {
return nil
}
if available > 0 {
limit = int(min(available, int64(int(^uint(0)>>1)))) limit = int(min(available, int64(int(^uint(0)>>1))))
encodedLimit = limit encodedLimit = limit
} }
} }
if strconv.IntSize == 32 {
limit = 64 << 20
}
base, cancel := context.WithCancel(e.output.ctx) base, cancel := context.WithCancel(e.output.ctx)
ctx := outputImageContext{Context: base, parent: e.output.ctx} ctx := outputImageContext{Context: base, parent: e.output.ctx}
var group sync.WaitGroup var group sync.WaitGroup
+14 -5
View File
@@ -52,6 +52,7 @@ type ConvertProgress struct {
// PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面 // PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面
// SkipUnchanged仅在未压缩、源格式与目标相同且未选页时不写出数据 // SkipUnchanged仅在未压缩、源格式与目标相同且未选页时不写出数据
// Pool可由多个独立转换共享,nil时不限制文件间并发 // Pool可由多个独立转换共享,nil时不限制文件间并发
// Resources提供共享计算与缓存策略,显式Pool优先控制计算调度
// 整份PDF同格式压缩直接重写PDF对象,其余PDF先转换为OFD对象再输出 // 整份PDF同格式压缩直接重写PDF对象,其余PDF先转换为OFD对象再输出
// RendererOptions同时用于PDF局部合成和输出,在PDF.RendererOptions之后应用 // RendererOptions同时用于PDF局部合成和输出,在PDF.RendererOptions之后应用
// Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置 // Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置
@@ -60,6 +61,7 @@ type ConvertProgress struct {
// PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil // PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil
// 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换 // 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换
type ConvertOptions struct { type ConvertOptions struct {
Resources *Resources
Input string Input string
Format string Format string
Pages []int Pages []int
@@ -145,6 +147,13 @@ func OutputFormats() []OutputFormat {
// 入参: ctx 取消上下文, input 输入源, output 输出流, options 转换选项 // 入参: ctx 取消上下文, input 输入源, output 输出流, options 转换选项
// 返回: ConvertReport 转换报告, error 错误信息 // 返回: ConvertReport 转换报告, error 错误信息
func Convert(ctx context.Context, input *Input, output io.Writer, options ConvertOptions) (report ConvertReport, err error) { func Convert(ctx context.Context, input *Input, output io.Writer, options ConvertOptions) (report ConvertReport, err error) {
if options.Resources != nil {
if options.Pool == nil {
options.Pool = options.Resources.pool
}
options.ReaderOptions = append(slices.Clone(options.ReaderOptions), WithResources(options.Resources))
options.PDF.Resources = options.Resources
}
if options.Pool != nil { if options.Pool != nil {
if err = options.Pool.acquire(ctx, false, 0); err != nil { if err = options.Pool.acquire(ctx, false, 0); err != nil {
return report, err return report, err
@@ -534,7 +543,7 @@ func (p *ConversionPool) acquire(ctx context.Context, image bool, size int64) er
p.mu.Lock() p.mu.Lock()
allowed := p.active < p.options.Parallelism && (p.active == 0 || limit != 0) allowed := p.active < p.options.Parallelism && (p.active == 0 || limit != 0)
if image { if image {
allowed = p.images < runtime.GOMAXPROCS(0) && (p.images == 0 || limit < 0 || size <= limit-p.memory) allowed = p.images < p.options.Parallelism && (p.images == 0 || limit < 0 || size <= limit-p.memory)
} }
if allowed { if allowed {
if image { if image {
@@ -584,15 +593,15 @@ func (p *ConversionPool) release(image bool, size int64) {
} }
// conversionWorkers 按当前文件数量分配内部计算任务 // conversionWorkers 按当前文件数量分配内部计算任务
// 入参: ctx 转换上下文, fallback 非共享模式并行上限 // 入参: ctx 转换上下文
// 返回: int 内部并行数 // 返回: int 内部并行数
func conversionWorkers(ctx context.Context, fallback int) int { func conversionWorkers(ctx context.Context) int {
p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool) p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool)
if p == nil { if p == nil {
return min(fallback, runtime.GOMAXPROCS(0)) return runtime.GOMAXPROCS(0)
} }
p.mu.Lock() p.mu.Lock()
active := p.active active := p.active
p.mu.Unlock() p.mu.Unlock()
return max(1, runtime.GOMAXPROCS(0)/max(1, active)) return max(1, p.options.Parallelism/max(1, active))
} }
+2 -1
View File
@@ -59,7 +59,7 @@ type CryptoProvider interface {
EncryptKey(certificate, key []byte) ([]byte, error) EncryptKey(certificate, key []byte) ([]byte, error)
} }
// ReaderOption 阅读器的凭据与密码提供者选项 // ReaderOption 阅读器的凭据、密码提供者与资源选项
type ReaderOption func(*readerOptions) type ReaderOption func(*readerOptions)
// EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码 // EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码
@@ -92,6 +92,7 @@ type EncryptionInfo struct {
// readerOptions 保存打开文档时的会话参数 // readerOptions 保存打开文档时的会话参数
type readerOptions struct { type readerOptions struct {
resources *Resources
credentials []Credentials credentials []Credentials
provider CryptoProvider provider CryptoProvider
} }
+1
View File
@@ -113,6 +113,7 @@ func (r *Reader) open(options []ReaderOption) error {
for _, option := range options { for _, option := range options {
option(&settings) option(&settings)
} }
r.resources = settings.resources
if settings.provider == nil { if settings.provider == nil {
settings.provider = GMCryptoProvider{} settings.provider = GMCryptoProvider{}
} }
+29 -2
View File
@@ -50,6 +50,7 @@ type Editor struct {
Info DocInfo Info DocInfo
pages []PageContent pages []PageContent
resources []editorResource resources []editorResource
runtimeResources *Resources
fonts map[string]*FontResource fonts map[string]*FontResource
fontRegistration editorFontRegistration fontRegistration editorFontRegistration
fontDirs []string fontDirs []string
@@ -154,11 +155,13 @@ func (e *Editor) Backends() RenderBackends {
// NewEditor 创建空白文档,保存前至少添加一页 // NewEditor 创建空白文档,保存前至少添加一页
// 返回: *Editor 文档编辑器 // 返回: *Editor 文档编辑器
func NewEditor() *Editor { func NewEditor() *Editor {
resources, _ := NewResources(ResourceOptions{})
var id [16]byte var id [16]byte
rand.Read(id[:]) rand.Read(id[:])
id[6] = id[6]&0x0f | 0x40 id[6] = id[6]&0x0f | 0x40
id[8] = id[8]&0x3f | 0x80 id[8] = id[8]&0x3f | 0x80
return &Editor{ return &Editor{
runtimeResources: resources,
Info: DocInfo{ Info: DocInfo{
DocID: hex.EncodeToString(id[:]), DocID: hex.EncodeToString(id[:]),
CreationDate: time.Now().Format("2006-01-02"), CreationDate: time.Now().Format("2006-01-02"),
@@ -172,6 +175,30 @@ func NewEditor() *Editor {
} }
} }
// SetResources 替换后续操作的资源策略,需与编辑操作串行调用
// 已创建的阅读器和快照继续使用原策略,不修改文档或撤销记录
// 入参: resources 共享策略,nil使用默认策略
func (e *Editor) SetResources(resources *Resources) {
if resources == nil {
resources, _ = NewResources(ResourceOptions{})
}
if resources == e.runtimeResources {
return
}
e.runtimeResources = resources
e.snapshotArchive = nil
e.fontRenderer = nil
e.fontSourcesCache = newFontSourceCache(resources)
e.validatedPaths = nil
if e.source != nil {
source := *e.source
e.source = &source
e.source.reader = cloneEditorReader(e.source.reader)
e.source.reader.resources = resources
e.source.reader.pageContents = resourceCache[pageContentKey, PageContent](resources, 16<<20)
}
}
// PageCount 获取当前页数 // PageCount 获取当前页数
// 返回: int 页数 // 返回: int 页数
func (e *Editor) PageCount() int { func (e *Editor) PageCount() int {
@@ -773,7 +800,7 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
// 返回: *Renderer 度量器 // 返回: *Renderer 度量器
func (e *Editor) newRenderer(reader *Reader) *Renderer { func (e *Editor) newRenderer(reader *Reader) *Renderer {
if e.fontSourcesCache == nil { if e.fontSourcesCache == nil {
e.fontSourcesCache = newFontSourceCache() e.fontSourcesCache = newFontSourceCache(e.runtimeResources)
} }
renderer := NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends)) renderer := NewRenderer(reader, WithFontDirs(e.fontDirs...), WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
renderer.fontSourcesCache = e.fontSourcesCache.child() renderer.fontSourcesCache = e.fontSourcesCache.child()
@@ -1325,7 +1352,7 @@ func (v *editorValidation) creationPath(value string) error {
// 返回: *renderCache[string, bool] 校验缓存 // 返回: *renderCache[string, bool] 校验缓存
func (e *Editor) pathValidationCache() *renderCache[string, bool] { func (e *Editor) pathValidationCache() *renderCache[string, bool] {
if e.validatedPaths == nil { if e.validatedPaths == nil {
e.validatedPaths = &renderCache[string, bool]{limit: 8 << 20} e.validatedPaths = resourceCache[string, bool](e.runtimeResources, 8<<20)
} }
return e.validatedPaths return e.validatedPaths
} }
+1
View File
@@ -453,6 +453,7 @@ func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error
next.encryption = e.encryption next.encryption = e.encryption
next.saveTarget = e.saveTarget next.saveTarget = e.saveTarget
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
next.SetResources(e.runtimeResources)
next.validatedPaths = e.validatedPaths next.validatedPaths = e.validatedPaths
*e = *next *e = *next
} }
+1
View File
@@ -907,6 +907,7 @@ func (e *Editor) restoreResourceSnapshot(state Editor) {
state.clearFontResourceCache() state.clearFontResourceCache()
} }
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.SetResources(e.runtimeResources)
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
state.saveTarget = e.saveTarget state.saveTarget = e.saveTarget
state.fontRenderer, state.fontSourcesCache = nil, nil state.fontRenderer, state.fontSourcesCache = nil, nil
+2
View File
@@ -53,6 +53,7 @@ func (e *Editor) Transaction(edit func(*Editor) error) error {
e.clearFontResourceCache() e.clearFontResourceCache()
} }
e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS e.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS
e.SetResources(before.runtimeResources)
e.fontSourcesCache = before.fontSourcesCache e.fontSourcesCache = before.fontSourcesCache
if e.sameResourceView(&before) { if e.sameResourceView(&before) {
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
@@ -168,6 +169,7 @@ func (e *Editor) restoreTransaction(state Editor) {
state.clearFontResourceCache() state.clearFontResourceCache()
} }
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.SetResources(e.runtimeResources)
state.fontSourcesCache = e.fontSourcesCache state.fontSourcesCache = e.fontSourcesCache
state.validatedPaths = e.validatedPaths state.validatedPaths = e.validatedPaths
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
+1
View File
@@ -151,6 +151,7 @@ func (r *Reader) EditorContext(ctx context.Context) (*Editor, error) {
} }
} }
e := NewEditor() e := NewEditor()
e.runtimeResources = r.resources
e.encryption = r.encryption e.encryption = r.encryption
directory := cleanPackagePath(reader.RootDir) directory := cleanPackagePath(reader.RootDir)
e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), fallbackDocID: e.Info.DocID, directory: directory, pages: make(map[string]*editorSourcePage, len(doc.Pages.Page)), origins: make(map[string]*editorObjectOrigin)} e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), fallbackDocID: e.Info.DocID, directory: directory, pages: make(map[string]*editorSourcePage, len(doc.Pages.Page)), origins: make(map[string]*editorObjectOrigin)}
+1 -1
View File
@@ -817,7 +817,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
files = make(map[string][]byte) files = make(map[string][]byte)
} }
maps.Copy(files, parts) maps.Copy(files, parts)
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion, pageContents: e.source.reader.pageContents} reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion, pageContents: e.source.reader.pageContents, resources: e.runtimeResources}
if err := reader.initRootContext(ctx); err != nil { if err := reader.initRootContext(ctx); err != nil {
return nil, err return nil, err
} }
+17 -5
View File
@@ -269,23 +269,35 @@ func (r *Renderer) fontDigest(data []byte) [32]byte {
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态 // resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() { func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache() if r.fontSourcesCache != nil {
r.fontSourcesCache.collections.clear()
}
r.fontSourcesCache = newFontSourceCache(r.Reader.resources)
r.resetFontBackendCache() r.resetFontBackendCache()
} }
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存 // resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
func (r *Renderer) resetFontBackendCache() { func (r *Renderer) resetFontBackendCache() {
r.fontDigests.clear()
r.fontPreparations.clear()
r.fontMetrics.clear()
r.glyphOutlines.clear()
for _, state := range r.sharedBackendStates {
if cache, ok := state.(interface{ clear() }); ok {
cache.clear()
}
}
r.sharedBackendStates = make(map[any]any) r.sharedBackendStates = make(map[any]any)
r.preparedFonts = make(map[string]*PreparedFont) r.preparedFonts = make(map[string]*PreparedFont)
r.fontDigests = &renderCache[fontDataKey, [32]byte]{limit: 4 << 20} r.fontDigests = resourceCache[fontDataKey, [32]byte](r.Reader.resources, 4<<20)
r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 16 << 20} r.fontPreparations = resourceCache[[32]byte, PreparedFont](r.Reader.resources, 16<<20)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult) r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
if r.fontSourcesCache != nil { if r.fontSourcesCache != nil {
r.fontSourcesCache.fallback = ResolvedFont{} r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false r.fontSourcesCache.fallbackRead = false
} }
r.fontMetrics = &renderCache[[32]byte, FontMetrics]{limit: 32 << 20} r.fontMetrics = resourceCache[[32]byte, FontMetrics](r.Reader.resources, 32<<20)
r.glyphOutlines = &renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20} r.glyphOutlines = resourceCache[glyphOutlineKey, GeometryPath](r.Reader.resources, 16<<20)
for _, state := range r.backendStates { for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok { if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts() fonts.resetFonts()
+4 -3
View File
@@ -79,15 +79,16 @@ type fontSourceData struct {
} }
// newFontSourceCache 创建独立字体来源缓存 // newFontSourceCache 创建独立字体来源缓存
// 入参: resources 共享资源策略
// 返回: *fontSourceCache 字体来源缓存 // 返回: *fontSourceCache 字体来源缓存
func newFontSourceCache() *fontSourceCache { func newFontSourceCache(resources *Resources) *fontSourceCache {
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]basefont.FileCandidate), filesystems: make(map[int][]basefont.FileCandidate), data: make(map[fontSourceKey]fontSourceData), collections: &renderCache[fontSourceKey, []byte]{limit: 32 << 20}} return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]basefont.FileCandidate), filesystems: make(map[int][]basefont.FileCandidate), data: make(map[fontSourceKey]fontSourceData), collections: resourceCache[fontSourceKey, []byte](resources, 32<<20)}
} }
// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立 // child 复用相同来源的文件索引与数据,匹配和回退状态保持独立
// 返回: *fontSourceCache 独立文档缓存 // 返回: *fontSourceCache 独立文档缓存
func (c *fontSourceCache) child() *fontSourceCache { func (c *fontSourceCache) child() *fontSourceCache {
cache := newFontSourceCache() cache := newFontSourceCache(nil)
cache.directories, cache.filesystems, cache.data = c.directories, c.filesystems, c.data cache.directories, cache.filesystems, cache.data = c.directories, c.filesystems, c.data
cache.collections = c.collections cache.collections = c.collections
return cache return cache
+10 -2
View File
@@ -41,7 +41,9 @@ var ErrPDFPassword = pdfgo.ErrPassword
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护 // ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
// Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化 // Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化
// Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样 // Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样
// Resources用于导入、局部合成与返回编辑器的共享资源预算
type PDFImportOptions struct { type PDFImportOptions struct {
Resources *Resources
Password []byte Password []byte
PasswordUTF8 bool PasswordUTF8 bool
Backends *RenderBackends Backends *RenderBackends
@@ -68,6 +70,7 @@ type PDFImportReport struct {
// pdfImporter 保存单次转换的对象与资源状态 // pdfImporter 保存单次转换的对象与资源状态
type pdfImporter struct { type pdfImporter struct {
resources *Resources
ctx context.Context ctx context.Context
reader *pdfgo.Reader reader *pdfgo.Reader
editor *Editor editor *Editor
@@ -207,6 +210,10 @@ func (o pdfObjectOrigin) merge(other pdfObjectOrigin) pdfObjectOrigin {
// 入参: ctx 取消上下文, input 输入源, options 转换选项 // 入参: ctx 取消上下文, input 输入源, options 转换选项
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息 // 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Editor, PDFImportReport, error) { func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Editor, PDFImportReport, error) {
if options.Resources == nil {
options.Resources, _ = NewResources(ResourceOptions{})
}
ctx = options.Resources.context(ctx)
if input == nil { if input == nil {
return nil, PDFImportReport{}, fmt.Errorf("missing source") return nil, PDFImportReport{}, fmt.Errorf("missing source")
} }
@@ -219,7 +226,7 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
if !finite(options.RasterDPI) || options.RasterDPI < 0 { if !finite(options.RasterDPI) || options.RasterDPI < 0 {
return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI") return nil, PDFImportReport{}, fmt.Errorf("invalid PDF raster DPI")
} }
renderer := NewRenderer(&Reader{}, options.RendererOptions...) renderer := NewRenderer(&Reader{resources: options.Resources}, options.RendererOptions...)
if options.Backends != nil { if options.Backends != nil {
WithRenderBackends(*options.Backends)(renderer) WithRenderBackends(*options.Backends)(renderer)
} }
@@ -234,10 +241,11 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
} }
editor := NewEditor() editor := NewEditor()
editor.runtimeResources = options.Resources
editor.SetRenderBackends(renderer.Backends()) editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources() editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}} importer := pdfImporter{resources: options.Resources, ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}}
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText} readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText}
var diagnostics []pdfgo.Diagnostic var diagnostics []pdfgo.Diagnostic
var readWarning func(pdfgo.Diagnostic) var readWarning func(pdfgo.Diagnostic)
+2
View File
@@ -246,6 +246,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
} }
matrix, width, height := codecpdf.PageMatrix(page) matrix, width, height := codecpdf.PageMatrix(page)
editor := NewEditor() editor := NewEditor()
editor.SetResources(p.resources)
editor.SetRenderBackends(p.editor.Backends()) editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...) editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...) editor.SetFontFS(p.editor.fontFS...)
@@ -255,6 +256,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
return Box{}, err return Box{}, err
} }
local := pdfImporter{ local := pdfImporter{
resources: p.resources,
ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer,
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height, matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string), rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
+4 -3
View File
@@ -126,6 +126,7 @@ type pdfCompositeImageColor struct {
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容 // pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct { type pdfCompositeCache struct {
resources *Resources
geometry *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] geometry *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]
clips *renderCache[[32]byte, GeometryPath] clips *renderCache[[32]byte, GeometryPath]
bounds map[pdfCompositeKey]Box bounds map[pdfCompositeKey]Box
@@ -157,7 +158,7 @@ type pdfCompositeScratch struct {
// 返回: *pdfCompositeCache 当前页面缓存 // 返回: *pdfCompositeCache 当前页面缓存
func (p *pdfImporter) compositingCache() *pdfCompositeCache { func (p *pdfImporter) compositingCache() *pdfCompositeCache {
if p.compositeCache == nil { if p.compositeCache == nil {
p.compositeCache = &pdfCompositeCache{images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}, meshes: map[*pdfgo.MeshGradient][]codecpdf.MeshTriangle{}} p.compositeCache = &pdfCompositeCache{resources: p.resources, images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}, meshes: map[*pdfgo.MeshGradient][]codecpdf.MeshTriangle{}}
} }
return p.compositeCache return p.compositeCache
} }
@@ -166,7 +167,7 @@ func (p *pdfImporter) compositingCache() *pdfCompositeCache {
// 返回: *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] 单页场景缓存 // 返回: *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] 单页场景缓存
func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] { func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] {
if c.geometry == nil { if c.geometry == nil {
c.geometry = &renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]{limit: 64 << 20} c.geometry = resourceCache[pdfCompositeSceneKey, pdfCompositeGeometry](c.resources, 64<<20)
} }
return c.geometry return c.geometry
} }
@@ -175,7 +176,7 @@ func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, p
// 返回: *renderCache[[32]byte, GeometryPath] 裁剪缓存 // 返回: *renderCache[[32]byte, GeometryPath] 裁剪缓存
func (c *pdfCompositeCache) clipCache() *renderCache[[32]byte, GeometryPath] { func (c *pdfCompositeCache) clipCache() *renderCache[[32]byte, GeometryPath] {
if c.clips == nil { if c.clips == nil {
c.clips = &renderCache[[32]byte, GeometryPath]{limit: 8 << 20} c.clips = resourceCache[[32]byte, GeometryPath](c.resources, 8<<20)
} }
return c.clips return c.clips
} }
+1 -1
View File
@@ -108,7 +108,7 @@ func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
return true, nil return true, nil
} }
if p.formResources == nil || p.formResources.editor != p.editor { if p.formResources == nil || p.formResources.editor != p.editor {
p.formResources = &pdfFormCache{editor: p.editor, entries: renderCache[[32]byte, pdfFormResource]{limit: 32 << 20}} p.formResources = &pdfFormCache{editor: p.editor, entries: *resourceCache[[32]byte, pdfFormResource](p.resources, 32<<20)}
} }
cache := p.formResources cache := p.formResources
defer cache.reset() defer cache.reset()
+1 -1
View File
@@ -419,7 +419,7 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
return pdfClipPath{}, err return pdfClipPath{}, err
} }
if p.clipPaths == nil { if p.clipPaths == nil {
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20} p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
} }
if cached, ok := p.clipPaths.get(key); ok { if cached, ok := p.clipPaths.get(key); ok {
return cached, nil return cached, nil
+3 -2
View File
@@ -383,6 +383,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
return nil, box, err return nil, box, err
} }
editor := NewEditor() editor := NewEditor()
editor.SetResources(p.resources)
editor.SetRenderBackends(p.editor.Backends()) editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...) editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...) editor.SetFontFS(p.editor.fontFS...)
@@ -398,9 +399,9 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont) p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
} }
if p.clipPaths == nil { if p.clipPaths == nil {
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20} p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
} }
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins} local := pdfImporter{resources: p.resources, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
if err := build(&local); err != nil { if err := build(&local); err != nil {
return nil, box, err return nil, box, err
} }
+1 -1
View File
@@ -136,7 +136,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
if cacheable { if cacheable {
if p.imageContents == nil { if p.imageContents == nil {
p.imageContents = &renderCache[pdfImageContentKey, pdfImageContent]{limit: 8 << 20} p.imageContents = resourceCache[pdfImageContentKey, pdfImageContent](p.resources, 8<<20)
} }
if cached, found := p.imageContents.get(contentKey); found && cached.matches(source) { if cached, found := p.imageContents.get(contentKey); found && cached.matches(source) {
id = cached.id id = cached.id
+1 -1
View File
@@ -69,7 +69,7 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
return p.buildTilingPattern(source, base) return p.buildTilingPattern(source, base)
} }
if p.patternCells == nil { if p.patternCells == nil {
p.patternCells = &renderCache[pdfPatternCellKey, pdfPatternCell]{limit: pdfPatternCellLimit} p.patternCells = resourceCache[pdfPatternCellKey, pdfPatternCell](p.resources, pdfPatternCellLimit)
} }
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace} key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace}
if source.PaintType == 2 { if source.PaintType == 2 {
+2 -2
View File
@@ -887,7 +887,7 @@ func (p *pdfImporter) linearMeshTriangles(mesh *pdfgo.MeshGradient, index int) (
} }
cache := p.compositingCache() cache := p.compositingCache()
if cache.linearMeshes == nil { if cache.linearMeshes == nil {
cache.linearMeshes = &renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]{limit: 8 << 20} cache.linearMeshes = resourceCache[pdfLinearMeshKey, []pdfgo.MeshTriangle](p.resources, 8<<20)
} }
key := pdfLinearMeshKey{mesh: mesh, index: index} key := pdfLinearMeshKey{mesh: mesh, index: index}
if triangles, ok := cache.linearMeshes.get(key); ok { if triangles, ok := cache.linearMeshes.get(key); ok {
@@ -1047,7 +1047,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
} }
c.cache.meshes[mesh] = triangles c.cache.meshes[mesh] = triangles
} }
options := codecpdf.MeshRasterOptions{Matrix: c.importer.matrix, Origin: pdfgo.Point{X: c.box.X, Y: c.box.Y}, DPI: c.importer.rasterDPI, Width: c.width, Height: c.height, Workers: conversionWorkers(c.importer.ctx, 4), SoftMask: c.softMask} options := codecpdf.MeshRasterOptions{Matrix: c.importer.matrix, Origin: pdfgo.Point{X: c.box.X, Y: c.box.Y}, DPI: c.importer.rasterDPI, Width: c.width, Height: c.height, Workers: conversionWorkers(c.importer.ctx), SoftMask: c.softMask}
pixels, err := codecpdf.MeshPixels(c.importer.ctx, mesh, triangles, space, converter, options) pixels, err := codecpdf.MeshPixels(c.importer.ctx, mesh, triangles, space, converter, options)
if err != nil { if err != nil {
return nil, err return nil, err
+8 -4
View File
@@ -37,6 +37,7 @@ type Reader struct {
OFD *OFD OFD *OFD
RootDir string RootDir string
ResMap map[string]string ResMap map[string]string
resources *Resources
resourcesRead map[string]bool resourcesRead map[string]bool
resourceFiles map[string]string resourceFiles map[string]string
imageCatalog []ImageInfo imageCatalog []ImageInfo
@@ -113,7 +114,7 @@ func (r *Reader) Fork(ctx context.Context) (*Reader, error) {
return nil, err return nil, err
} }
if r.pageContents == nil { if r.pageContents == nil {
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20} r.pageContents = resourceCache[pageContentKey, PageContent](r.resources, 16<<20)
} }
next := *r next := *r
next.Closer = nil next.Closer = nil
@@ -210,7 +211,7 @@ func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error
if index < 0 || index >= r.DocumentCount() { if index < 0 || index >= r.DocumentCount() {
return nil, fmt.Errorf("document index %d out of range", index) return nil, fmt.Errorf("document index %d out of range", index)
} }
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index} next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index, resources: r.resources}
if err := next.initRootContext(ctx); err != nil { if err := next.initRootContext(ctx); err != nil {
return nil, err return nil, err
} }
@@ -762,6 +763,9 @@ func (r *Reader) initRoot() error {
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: error 索引、读取、解析或取消错误 // 返回: error 索引、读取、解析或取消错误
func (r *Reader) initRootContext(ctx context.Context) error { func (r *Reader) initRootContext(ctx context.Context) error {
if r.resources == nil {
r.resources, _ = NewResources(ResourceOptions{})
}
view := *r view := *r
if err := view.indexPackageContext(ctx); err != nil { if err := view.indexPackageContext(ctx); err != nil {
return err return err
@@ -780,7 +784,7 @@ func (r *Reader) initRootContext(ctx context.Context) error {
} }
r.fileIndex, r.fileNames = view.fileIndex, view.fileNames r.fileIndex, r.fileNames = view.fileIndex, view.fileNames
if r.pageContents == nil { if r.pageContents == nil {
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20} r.pageContents = resourceCache[pageContentKey, PageContent](r.resources, 16<<20)
} }
r.OFD = &ofd r.OFD = &ofd
r.doc, r.versionInfo = nil, nil r.doc, r.versionInfo = nil, nil
@@ -1100,7 +1104,7 @@ func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, templ
return nil, err return nil, err
} }
if r.pageContents == nil { if r.pageContents == nil {
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20} r.pageContents = resourceCache[pageContentKey, PageContent](r.resources, 16<<20)
} }
var content PageContent var content PageContent
if len(data) > (r.pageContents.limit/4-4096)/16 { if len(data) > (r.pageContents.limit/4-4096)/16 {
+242 -21
View File
@@ -21,13 +21,49 @@ import (
"image" "image"
"reflect" "reflect"
"sync" "sync"
"time"
"weak"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
// defaultResourceCacheBytes 定义默认缓存总预算及类别扩展基准
const defaultResourceCacheBytes = 256 << 20
// ResourceOptions 设置共享计算、派生缓存与快照预算,字节预算不限制进程实际分配
// Parallelism为0时使用GOMAXPROCS,MemoryLimit为0时不指定计算预算
// CacheBytes为0时使用256MiB,SnapshotBytes为0时使用缓存预算的一半,负值禁用相应缓存
// AvailableMemory返回可用于新增任务的字节数,同时作为缓存动态上限,需支持并发且不可重入Resources
type ResourceOptions struct {
Parallelism int
MemoryLimit int64
CacheBytes int64
SnapshotBytes int64
AvailableMemory func() int64
}
// Resources 协调共享缓存与计算任务,通过NewResources创建,可跨阅读器使用,不可复制
// 缓存仅持有存活实例的弱引用,Clear释放派生数据,不关闭输入或取消任务
type Resources struct {
options ResourceOptions
pool *ConversionPool
mu sync.Mutex
used int64
limit int64
checked time.Time
order list.List
}
// resourceEntry 保存共享缓存成本与弱引用淘汰操作
type resourceEntry struct {
cost int64
evict func()
}
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
type renderCache[K comparable, V any] struct { type renderCache[K comparable, V any] struct {
mu sync.Mutex mu sync.Mutex
resources *Resources
limit, used int limit, used int
entries map[K]*list.Element entries map[K]*list.Element
order list.List order list.List
@@ -46,16 +82,175 @@ type renderCacheEntry[K comparable, V any] struct {
key K key K
value V value V
cost int cost int
shared *list.Element
}
// NewResources 创建共享资源策略,不启动任务或修改进程全局配置
// 入参: options 并行及内存选项
// 返回: *Resources 共享策略, error 参数错误
func NewResources(options ResourceOptions) (*Resources, error) {
pool, err := NewConversionPool(ConversionPoolOptions{Parallelism: options.Parallelism, MemoryLimit: options.MemoryLimit, AvailableMemory: options.AvailableMemory})
if err != nil {
return nil, err
}
if options.CacheBytes == 0 {
options.CacheBytes = defaultResourceCacheBytes
}
options.CacheBytes = max(0, options.CacheBytes)
if options.SnapshotBytes == 0 {
options.SnapshotBytes = options.CacheBytes / 2
}
options.SnapshotBytes = max(0, min(options.SnapshotBytes, options.CacheBytes))
return &Resources{options: options, pool: pool, limit: options.CacheBytes}, nil
}
// ConversionPool 返回资源策略共享的转换调度器
// 返回: *ConversionPool 转换调度器
func (r *Resources) ConversionPool() *ConversionPool { return r.pool }
// Acquire 等待一个计算任务及其估算工作集额度,允许超预算任务独占执行
// 调用方必须调用返回的释放函数,重复释放无效,不可持有额度再嵌套申请
// 入参: ctx 等待上下文, memory 估算字节数,零值仅限制并行
// 返回: func() 释放额度, error 参数或取消错误
func (r *Resources) Acquire(ctx context.Context, memory int64) (func(), error) {
if memory < 0 {
return nil, errors.New("invalid task memory")
}
if err := r.pool.acquire(ctx, true, memory); err != nil {
return nil, err
}
var once sync.Once
return func() { once.Do(func() { r.pool.release(true, memory) }) }, nil
}
// context 为未指定转换池的操作附加资源调度器
// 入参: ctx 操作上下文
// 返回: context.Context 计算上下文
func (r *Resources) context(ctx context.Context) context.Context {
if r == nil || ctx.Value(conversionPoolKey{}) != nil {
return ctx
}
return context.WithValue(ctx, conversionPoolKey{}, r.pool)
}
// WithResources 设置阅读器及派生编辑器共享的资源策略
// 入参: resources 共享策略,nil使用默认策略
// 返回: ReaderOption 阅读选项
func WithResources(resources *Resources) ReaderOption {
return func(options *readerOptions) { options.resources = resources }
}
// Clear 释放当前缓存,正在计算的任务完成后仍可写入新缓存
func (r *Resources) Clear() {
r.mu.Lock()
defer r.mu.Unlock()
for r.order.Len() != 0 {
r.evict()
}
}
// CacheUsage 返回当前登记的派生缓存估算字节数,不含任务临时内存
// 返回: int64 缓存成本
func (r *Resources) CacheUsage() int64 {
r.mu.Lock()
defer r.mu.Unlock()
return r.used
}
// refresh 按当前可用内存收缩缓存,需持有资源锁
func (r *Resources) refresh() {
if r.options.AvailableMemory == nil || time.Since(r.checked) < 250*time.Millisecond {
return
}
r.checked = time.Now()
r.limit = min(r.options.CacheBytes, max(0, r.options.AvailableMemory()))
for r.used > r.limit {
r.evict()
}
}
// evict 淘汰最久未访问的共享条目,需持有资源锁
func (r *Resources) evict() {
item := r.order.Back()
entry := item.Value.(resourceEntry)
r.order.Remove(item)
r.used -= entry.cost
entry.evict()
}
// resourceCache 按类别基准分配共享预算,较大总预算同比扩展类别额度
// 入参: resources 共享策略, baseline 类别基准预算
// 返回: *renderCache 缓存实例
func resourceCache[K comparable, V any](resources *Resources, baseline int) *renderCache[K, V] {
if resources == nil {
return &renderCache[K, V]{limit: baseline}
}
limit := min(resources.options.CacheBytes, int64(int(^uint(0)>>1)-1))
if baseline > 0 && limit > int64(baseline) {
share := int64(baseline)
if limit > defaultResourceCacheBytes {
scaled := float64(baseline) * (float64(limit) / defaultResourceCacheBytes)
share = limit
if scaled < float64(limit) {
share = int64(scaled)
}
}
limit = min(limit, share)
}
return &renderCache[K, V]{resources: resources, limit: int(limit)}
}
// mutex 返回缓存数据及共享账本使用的同一把锁
// 返回: *sync.Mutex 缓存锁
func (c *renderCache[K, V]) mutex() *sync.Mutex {
if c.resources != nil {
return &c.resources.mu
}
return &c.mu
}
// touch 更新本地及共享条目的访问顺序,需持有缓存锁
// 入参: item 缓存条目
func (c *renderCache[K, V]) touch(item *list.Element) {
c.order.MoveToFront(item)
if shared := item.Value.(renderCacheEntry[K, V]).shared; shared != nil {
c.resources.order.MoveToFront(shared)
}
}
// remove 删除本地条目并归还共享预算,需持有缓存锁
// 入参: item 缓存条目
func (c *renderCache[K, V]) remove(item *list.Element) {
entry := item.Value.(renderCacheEntry[K, V])
c.used -= entry.cost
c.order.Remove(item)
delete(c.entries, entry.key)
if entry.shared != nil {
c.resources.used -= int64(entry.cost)
c.resources.order.Remove(entry.shared)
}
}
// clear 释放已有缓存条目并归还共享预算,不取消正在加载的任务
func (c *renderCache[K, V]) clear() {
if c == nil {
return
}
c.mutex().Lock()
defer c.mutex().Unlock()
for c.order.Len() != 0 {
c.remove(c.order.Back())
}
} }
// get 读取缓存并更新使用顺序 // get 读取缓存并更新使用顺序
// 入参: key 缓存键 // 入参: key 缓存键
// 返回: V 缓存值, bool 是否命中 // 返回: V 缓存值, bool 是否命中
func (c *renderCache[K, V]) get(key K) (V, bool) { func (c *renderCache[K, V]) get(key K) (V, bool) {
c.mu.Lock() c.mutex().Lock()
defer c.mu.Unlock() defer c.mutex().Unlock()
if item := c.entries[key]; item != nil { if item := c.entries[key]; item != nil {
c.order.MoveToFront(item) c.touch(item)
return item.Value.(renderCacheEntry[K, V]).value, true return item.Value.(renderCacheEntry[K, V]).value, true
} }
var zero V var zero V
@@ -65,12 +260,10 @@ func (c *renderCache[K, V]) get(key K) (V, bool) {
// put 缓存预算内的派生数据,超大单项不进入缓存 // put 缓存预算内的派生数据,超大单项不进入缓存
// 入参: key 缓存键, value 缓存值, cost 字节成本 // 入参: key 缓存键, value 缓存值, cost 字节成本
func (c *renderCache[K, V]) put(key K, value V, cost int) { func (c *renderCache[K, V]) put(key K, value V, cost int) {
c.mu.Lock() c.mutex().Lock()
defer c.mu.Unlock() defer c.mutex().Unlock()
if item := c.entries[key]; item != nil { if item := c.entries[key]; item != nil {
c.used -= item.Value.(renderCacheEntry[K, V]).cost c.remove(item)
c.order.Remove(item)
delete(c.entries, key)
} }
if cost <= 0 || cost > c.limit { if cost <= 0 || cost > c.limit {
return return
@@ -78,15 +271,43 @@ func (c *renderCache[K, V]) put(key K, value V, cost int) {
if c.entries == nil { if c.entries == nil {
c.entries = make(map[K]*list.Element) c.entries = make(map[K]*list.Element)
} }
for c.used+cost > c.limit { for cost > c.limit-c.used {
item := c.order.Back() item := c.order.Back()
entry := item.Value.(renderCacheEntry[K, V]) c.remove(item)
c.used -= entry.cost
delete(c.entries, entry.key)
c.order.Remove(item)
} }
c.entries[key] = c.order.PushFront(renderCacheEntry[K, V]{key, value, cost}) entry := renderCacheEntry[K, V]{key: key, value: value, cost: cost}
if r := c.resources; r != nil {
r.refresh()
if int64(cost) > r.limit {
return
}
for int64(cost) > r.limit-r.used {
r.evict()
}
}
item := c.order.PushFront(entry)
c.entries[key] = item
c.used += cost c.used += cost
if r := c.resources; r != nil {
ref, entryRef := weak.Make(c), weak.Make(item)
entry.shared = r.order.PushFront(resourceEntry{cost: int64(cost), evict: func() {
if cache := ref.Value(); cache != nil {
previous := entryRef.Value()
if previous == nil {
return
}
entry := previous.Value.(renderCacheEntry[K, V])
if cache.entries[entry.key] != previous {
return
}
cache.used -= entry.cost
cache.order.Remove(previous)
delete(cache.entries, entry.key)
}
}})
item.Value = entry
r.used += int64(cost)
}
} }
// load 复用缓存并合并并发请求,等待取消不影响正在读取的任务 // load 复用缓存并合并并发请求,等待取消不影响正在读取的任务
@@ -101,15 +322,15 @@ func (c *renderCache[K, V]) load(ctx context.Context, key K, read func() (V, int
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return zero, err return zero, err
} }
c.mu.Lock() c.mutex().Lock()
if item := c.entries[key]; item != nil { if item := c.entries[key]; item != nil {
c.order.MoveToFront(item) c.touch(item)
value := item.Value.(renderCacheEntry[K, V]).value value := item.Value.(renderCacheEntry[K, V]).value
c.mu.Unlock() c.mutex().Unlock()
return value, nil return value, nil
} }
if pending := c.loading[key]; pending != nil { if pending := c.loading[key]; pending != nil {
c.mu.Unlock() c.mutex().Unlock()
select { select {
case <-ctx.Done(): case <-ctx.Done():
return zero, ctx.Err() return zero, ctx.Err()
@@ -125,12 +346,12 @@ func (c *renderCache[K, V]) load(ctx context.Context, key K, read func() (V, int
} }
pending := &renderCacheLoad[V]{done: make(chan struct{}), err: errors.New("resource loading interrupted")} pending := &renderCacheLoad[V]{done: make(chan struct{}), err: errors.New("resource loading interrupted")}
c.loading[key] = pending c.loading[key] = pending
c.mu.Unlock() c.mutex().Unlock()
defer func() { defer func() {
c.mu.Lock() c.mutex().Lock()
delete(c.loading, key) delete(c.loading, key)
close(pending.done) close(pending.done)
c.mu.Unlock() c.mutex().Unlock()
}() }()
value, cost, err := read() value, cost, err := read()
if err == nil { if err == nil {
+1 -1
View File
@@ -77,7 +77,7 @@ func newSemanticCompiler(r *Renderer) (*semanticCompiler, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &semanticCompiler{renderer: r, geometry: geometry, patterns: &renderCache[patternCellKey, []RasterCommand]{limit: 8 << 20}}, nil return &semanticCompiler{renderer: r, geometry: geometry, patterns: resourceCache[patternCellKey, []RasterCommand](r.Reader.resources, 8<<20)}, nil
} }
// segments 转换光栅曲线,仅在输出边界展开椭圆弧 // segments 转换光栅曲线,仅在输出边界展开椭圆弧
+4 -2
View File
@@ -34,6 +34,7 @@ type stampContentKey struct {
// stampContentState 保存有界的共享签章编译结果及配置 // stampContentState 保存有界的共享签章编译结果及配置
type stampContentState struct { type stampContentState struct {
resources *Resources
config RenderBackends config RenderBackends
cache renderCache[stampContentKey, []RasterCommand] cache renderCache[stampContentKey, []RasterCommand]
} }
@@ -52,7 +53,8 @@ type patternCellKey struct {
// resetFonts 使包含文字的签章编译结果随字体配置失效 // resetFonts 使包含文字的签章编译结果随字体配置失效
func (s *stampContentState) resetFonts() { func (s *stampContentState) resetFonts() {
s.cache = renderCache[stampContentKey, []RasterCommand]{limit: 16 << 20} s.cache.clear()
s.cache = *resourceCache[stampContentKey, []RasterCommand](s.resources, 16<<20)
} }
// sharedStamp 复用相同签章的解码、嵌套文档编译及局部裁剪,不缓存放置位置 // sharedStamp 复用相同签章的解码、嵌套文档编译及局部裁剪,不缓存放置位置
@@ -74,7 +76,7 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
config := r.backends config := r.backends
if state == nil || !sameBackend(state.config.Fonts, config.Fonts) || !sameBackend(state.config.Geometry, config.Geometry) || if state == nil || !sameBackend(state.config.Fonts, config.Fonts) || !sameBackend(state.config.Geometry, config.Geometry) ||
!sameBackend(state.config.Compiler, config.Compiler) || !sameBackend(state.config.Raster, config.Raster) { !sameBackend(state.config.Compiler, config.Compiler) || !sameBackend(state.config.Raster, config.Raster) {
state = &stampContentState{config: config} state = &stampContentState{config: config, resources: r.Reader.resources}
state.resetFonts() state.resetFonts()
r.backendStates[stampContentStateKey{}] = state r.backendStates[stampContentStateKey{}] = state
} }
+7 -1
View File
@@ -89,6 +89,11 @@ func (r *Renderer) Fork(ctx context.Context) (*Renderer, error) {
next.fontPreparations = r.fontPreparations next.fontPreparations = r.fontPreparations
next.fontMetrics = r.fontMetrics next.fontMetrics = r.fontMetrics
next.glyphOutlines = r.glyphOutlines next.glyphOutlines = r.glyphOutlines
next.sharedBackendStates[canvasPathCacheKey{}] = r.canvasParsedPaths()
next.sharedBackendStates[canvasClipCacheKey{}] = r.canvasClipIntersections()
next.sharedBackendStates[canvasFillCacheKey{}] = r.canvasSettledPaths()
next.sharedBackendStates[canvasDrawClipCacheKey{}] = r.canvasDrawClips()
next.sharedBackendStates[svgResourceCacheKey{}] = r.svgImageResources()
return next, nil return next, nil
} }
@@ -108,8 +113,9 @@ func (r *Renderer) deviceLineWidth(width float64) float64 {
// ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置 // ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置
func (r *Renderer) ClearCache() { func (r *Renderer) ClearCache() {
r.imageCache.clear()
r.backendStates = make(map[any]any) r.backendStates = make(map[any]any)
r.imageCache = &renderCache[string, image.Image]{limit: imageCacheLimit} r.imageCache = resourceCache[string, image.Image](r.Reader.resources, imageCacheLimit)
r.templatePageCache = make(map[string]*PageContent) r.templatePageCache = make(map[string]*PageContent)
r.resetFontCache() r.resetFontCache()
} }
+9 -9
View File
@@ -42,7 +42,7 @@ func (r *clipRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canva
r.add(applyClipPath(path.Copy().Transform(m), r.clip)) r.add(applyClipPath(path.Copy().Transform(m), r.clip))
} }
if style.HasStroke() { if style.HasStroke() {
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance) p := strokeCanvasPath(path.Dash(style.DashOffset, style.Dashes...), style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
r.add(applyClipPath(p.Transform(m), r.clip)) r.add(applyClipPath(p.Transform(m), r.clip))
} }
} }
@@ -164,7 +164,7 @@ func (r *Renderer) buildClipAreaPath(obj PathObject, drawParam string, parent Ma
} }
result = paths.Merge() result = paths.Merge()
if obj.Rule == "Even-Odd" { if obj.Rule == "Even-Odd" {
result = result.Settle(canvas.EvenOdd) result = settleCanvasPath(result, canvas.EvenOdd)
} }
} }
if stroke { if stroke {
@@ -255,12 +255,12 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
return result return result
} }
if result, ok := intersectLinearCanvasContours(parent, current); ok { if result, ok := intersectLinearCanvasContours(parent, current); ok {
return result.Settle(canvas.NonZero) return settleCanvasPath(result, canvas.NonZero)
} }
if result, ok := intersectLinearCanvasContours(current, parent); ok { if result, ok := intersectLinearCanvasContours(current, parent); ok {
return result.Settle(canvas.NonZero) return settleCanvasPath(result, canvas.NonZero)
} }
return parent.And(current) return combineCanvasPaths(parent, current, "and")
} }
// intersectConvexCanvasPaths 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线 // intersectConvexCanvasPaths 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线
@@ -396,7 +396,7 @@ func unionClipPath(left, right *canvas.Path) *canvas.Path {
return bounds.ToPath() return bounds.ToPath()
} }
} }
return left.Or(right) return combineCanvasPaths(left, right, "or")
} }
// applyClipPath 应用裁剪路径 // applyClipPath 应用裁剪路径
@@ -432,12 +432,12 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
return result return result
} }
if result, ok := intersectLinearCanvasContours(path, clip); ok { if result, ok := intersectLinearCanvasContours(path, clip); ok {
return result.Settle(canvas.NonZero) return settleCanvasPath(result, canvas.NonZero)
} }
if result, ok := intersectLinearCanvasContours(clip, path); ok { if result, ok := intersectLinearCanvasContours(clip, path); ok {
return result.Settle(canvas.NonZero) return settleCanvasPath(result, canvas.NonZero)
} }
return path.And(clip) return combineCanvasPaths(path, clip, "and")
} }
// pruneCanvasContours 排除与裁剪边界分离的完整轮廓,保留相交轮廓的曲线和绕向 // pruneCanvasContours 排除与裁剪边界分离的完整轮廓,保留相交轮廓的曲线和绕向
+45 -5
View File
@@ -20,10 +20,50 @@ import (
"math" "math"
"slices" "slices"
"strconv" "strconv"
"sync"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
) )
// canvasGeometryMu 保护底层路径运算的共享扫描线对象池
var canvasGeometryMu sync.Mutex
// settleCanvasPath 在共享对象池保护下归整路径
// 入参: path 路径, rule 填充规则
// 返回: *canvas.Path 归整路径
func settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
return path.Settle(rule)
}
// combineCanvasPaths 在共享对象池保护下计算路径布尔运算
// 入参: left 左路径, right 右路径, operation 运算名称
// 返回: *canvas.Path 运算结果
func combineCanvasPaths(left, right *canvas.Path, operation string) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
switch operation {
case "or":
return left.Or(right)
case "not":
return left.Not(right)
case "xor":
return left.Xor(right)
default:
return left.And(right)
}
}
// strokeCanvasPath 在共享对象池保护下生成描边轮廓
// 入参: path 路径, width 线宽, cap 端点, join 连接, tolerance 容差
// 返回: *canvas.Path 描边轮廓
func strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) *canvas.Path {
canvasGeometryMu.Lock()
defer canvasGeometryMu.Unlock()
return path.Stroke(width, cap, join, tolerance)
}
// Curves 在输出边界转换椭圆弧,其他曲线保持原样 // Curves 在输出边界转换椭圆弧,其他曲线保持原样
// 入参: path 页面路径 // 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 路径错误 // 返回: GeometryPath 贝塞尔路径, error 路径错误
@@ -108,7 +148,7 @@ func (CanvasBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, e
if evenOdd { if evenOdd {
rule = canvas.EvenOdd rule = canvas.EvenOdd
} }
p = p.Settle(rule) p = settleCanvasPath(p, rule)
if p.Empty() { if p.Empty() {
return nil, nil return nil, nil
} }
@@ -132,11 +172,11 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
case GeometryIntersect: case GeometryIntersect:
result = applyClipPath(a, b) result = applyClipPath(a, b)
case GeometryUnion: case GeometryUnion:
result = a.Or(b) result = combineCanvasPaths(a, b, "or")
case GeometrySubtract: case GeometrySubtract:
result = a.Not(b) result = combineCanvasPaths(a, b, "not")
case GeometryXor: case GeometryXor:
result = a.Xor(b) result = combineCanvasPaths(a, b, "xor")
default: default:
return nil, fmt.Errorf("unsupported geometry operation %d", operation) return nil, fmt.Errorf("unsupported geometry operation %d", operation)
} }
@@ -178,7 +218,7 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
style.lineCap = pathLineCap(options.Cap, style.lineCap) style.lineCap = pathLineCap(options.Cap, style.lineCap)
p = p.Dash(options.DashOffset, options.Dashes...) p = p.Dash(options.DashOffset, options.Dashes...)
p = flattenCanvasPath(p, tolerance) p = flattenCanvasPath(p, tolerance)
p = p.Stroke(options.Width, style.lineCap, style.lineJoin, tolerance) p = strokeCanvasPath(p, options.Width, style.lineCap, style.lineJoin, tolerance)
return *geometryFromCanvasPath(p), nil return *geometryFromCanvasPath(p), nil
} }
+1 -1
View File
@@ -106,7 +106,7 @@ func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m can
r.addContour(path.Copy().Transform(m), style.FillRule == canvas.EvenOdd) r.addContour(path.Copy().Transform(m), style.FillRule == canvas.EvenOdd)
} }
if style.HasStroke() { if style.HasStroke() {
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance) p := strokeCanvasPath(path.Dash(style.DashOffset, style.Dashes...), style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
r.add(p.Transform(m)) r.add(p.Transform(m))
} }
} }
+2 -2
View File
@@ -151,10 +151,10 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
} }
start := buf.Len() start := buf.Len()
p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil) p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil)
renderer := &pdfRenderer{PDF: p, glyphPaths: renderCache[*canvas.Path, *canvas.Path]{limit: 16 << 20}, images: make([][]image.Image, 1), text: make([]codecpdf.TextPage, 1), navigation: navigation} renderer := &pdfRenderer{PDF: p, glyphPaths: *resourceCache[*canvas.Path, *canvas.Path](r.Reader.resources, 16<<20), images: make([][]image.Image, 1), text: make([]codecpdf.TextPage, 1), navigation: navigation}
defer func() { defer func() {
if renderer.imageError != nil { if renderer.imageError != nil {
r.canvasState().images = renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit} r.canvasState().images = *resourceCache[*EncodedImage, image.Image](r.Reader.resources, imageCacheLimit)
} }
}() }()
var info DocInfo var info DocInfo
+3 -3
View File
@@ -127,7 +127,7 @@ func resolveShdPaint(ctx *canvas.Context, paint any) (any, *canvas.Path, canvas.
if g.gradient.Mesh != nil && g.gradient.Mesh.Background.A == 0 { if g.gradient.Mesh != nil && g.gradient.Mesh.Background.A == 0 {
outline := g.gradient.Mesh.outline() outline := g.gradient.Mesh.outline()
clip, _ := canvasObjectPath(outline) clip, _ := canvasObjectPath(outline)
return g, clip.Settle(canvas.NonZero).Transform(s.view), s.view return g, settleCanvasPath(clip, canvas.NonZero).Transform(s.view), s.view
} }
return g, nil, s.view return g, nil, s.view
case *canvas.LinearGradient: case *canvas.LinearGradient:
@@ -259,7 +259,7 @@ func (r *Renderer) drawShdPath(ctx *canvas.Context, path *canvas.Path, view canv
func drawRepeatAxialPath(ctx *canvas.Context, path *canvas.Path, gradient *repeatAxialGradient, view canvas.Matrix) { func drawRepeatAxialPath(ctx *canvas.Context, path *canvas.Path, gradient *repeatAxialGradient, view canvas.Matrix) {
p := path.Copy() p := path.Copy()
if ctx.FillRule == canvas.EvenOdd { if ctx.FillRule == canvas.EvenOdd {
p = p.Settle(canvas.EvenOdd) p = settleCanvasPath(p, canvas.EvenOdd)
} }
p = p.Transform(view.Inv()) p = p.Transform(view.Inv())
d := gradient.End.Sub(gradient.Start).Mul(gradient.period) d := gradient.End.Sub(gradient.Start).Mul(gradient.period)
@@ -335,7 +335,7 @@ func radialShdClip(g *canvas.RadialGradient, extend int, bounds canvas.Rect) *ca
clip = bounds.ToPath() clip = bounds.ToPath()
} }
if extend&1 == 0 && r0 > 0 { if extend&1 == 0 && r0 > 0 {
clip = clip.Not(canvas.Circle(r0).Translate(inner.X, inner.Y)) clip = combineCanvasPaths(clip, canvas.Circle(r0).Translate(inner.X, inner.Y), "not")
} }
return clip return clip
} }
+49 -9
View File
@@ -40,6 +40,9 @@ type canvasPathCacheKey struct{}
// canvasFillCacheKey 区分按填充规则规范化的共享路径缓存 // canvasFillCacheKey 区分按填充规则规范化的共享路径缓存
type canvasFillCacheKey struct{} type canvasFillCacheKey struct{}
// canvasDrawClipCacheKey 区分绘制轮廓的共享裁剪缓存
type canvasDrawClipCacheKey struct{}
// canvasOutlineCacheKey 区分子渲染器共享的字体轮廓解析缓存 // canvasOutlineCacheKey 区分子渲染器共享的字体轮廓解析缓存
type canvasOutlineCacheKey struct{} type canvasOutlineCacheKey struct{}
@@ -57,6 +60,7 @@ type canvasPathKey struct {
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存 // canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
type canvasBackendState struct { type canvasBackendState struct {
resources *Resources
images renderCache[*EncodedImage, image.Image] images renderCache[*EncodedImage, image.Image]
fontMap map[string]*canvas.FontFamily fontMap map[string]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont svgFontCache map[*canvas.Font]SVGFont
@@ -74,7 +78,7 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path
if r.sharedBackendStates == nil { if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any) r.sharedBackendStates = make(map[any]any)
} }
cache := &renderCache[[32]byte, *canvas.Path]{limit: 8 << 20} cache := resourceCache[[32]byte, *canvas.Path](r.Reader.resources, 8<<20)
r.sharedBackendStates[key] = cache r.sharedBackendStates[key] = cache
return cache return cache
} }
@@ -89,7 +93,7 @@ func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path]
if r.sharedBackendStates == nil { if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any) r.sharedBackendStates = make(map[any]any)
} }
cache := &renderCache[canvasPathKey, *canvas.Path]{limit: 16 << 20} cache := resourceCache[canvasPathKey, *canvas.Path](r.Reader.resources, 16<<20)
r.sharedBackendStates[key] = cache r.sharedBackendStates[key] = cache
return cache return cache
} }
@@ -104,7 +108,7 @@ func (r *Renderer) canvasSettledPaths() *renderCache[canvasFillKey, *canvas.Path
if r.sharedBackendStates == nil { if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any) r.sharedBackendStates = make(map[any]any)
} }
cache := &renderCache[canvasFillKey, *canvas.Path]{limit: 16 << 20} cache := resourceCache[canvasFillKey, *canvas.Path](r.Reader.resources, 16<<20)
r.sharedBackendStates[key] = cache r.sharedBackendStates[key] = cache
return cache return cache
} }
@@ -116,11 +120,45 @@ func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.Font
if state, ok := r.sharedBackendStates[key]; ok { if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[canvasFontKey, *canvas.FontFamily]) return state.(*renderCache[canvasFontKey, *canvas.FontFamily])
} }
cache := &renderCache[canvasFontKey, *canvas.FontFamily]{limit: 32 << 20} cache := resourceCache[canvasFontKey, *canvas.FontFamily](r.Reader.resources, 32<<20)
r.sharedBackendStates[key] = cache r.sharedBackendStates[key] = cache
return cache return cache
} }
// canvasDrawClips 获取派生渲染器共享的绘制裁剪缓存
// 返回: *renderCache 绘制裁剪缓存
func (r *Renderer) canvasDrawClips() *renderCache[[32]byte, *canvas.Path] {
key := canvasDrawClipCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[[32]byte, *canvas.Path])
}
cache := resourceCache[[32]byte, *canvas.Path](r.Reader.resources, 16<<20)
r.sharedBackendStates[key] = cache
return cache
}
// applyCachedClipPath 复用绘制轮廓的裁剪结果,返回可独立变换的副本
// 入参: path 绘制轮廓, clip 裁剪轮廓
// 返回: *canvas.Path 裁剪结果
func (r *Renderer) applyCachedClipPath(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil {
return path
}
if rect, ok := rectangularPath(clip); ok && rect.Contains(path.Bounds()) {
return reduceRepeatedNonZeroPath(path)
}
key := canvasGeometryDigest(path, clip)
result, err := r.canvasDrawClips().load(r.pageContext, key, func() (*canvas.Path, int, error) {
result := applyClipPath(path, clip).Copy()
return result, len(result.Data())*8 + 256, nil
})
if err != nil {
r.renderError = err
return &canvas.Path{}
}
return result.Copy()
}
// canvasFontOutlines 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量 // canvasFontOutlines 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量
// 返回: *renderCache[*canvas.Font, *basefont.Outliner] 字体轮廓缓存 // 返回: *renderCache[*canvas.Font, *basefont.Outliner] 字体轮廓缓存
func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *basefont.Outliner] { func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *basefont.Outliner] {
@@ -128,7 +166,7 @@ func (r *Renderer) canvasFontOutlines() *renderCache[*canvas.Font, *basefont.Out
if state, ok := r.sharedBackendStates[key]; ok { if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[*canvas.Font, *basefont.Outliner]) return state.(*renderCache[*canvas.Font, *basefont.Outliner])
} }
cache := &renderCache[*canvas.Font, *basefont.Outliner]{limit: 32 << 20} cache := resourceCache[*canvas.Font, *basefont.Outliner](r.Reader.resources, 32<<20)
r.sharedBackendStates[key] = cache r.sharedBackendStates[key] = cache
return cache return cache
} }
@@ -141,7 +179,8 @@ func (r *Renderer) canvasState() *canvasBackendState {
return state.(*canvasBackendState) return state.(*canvasBackendState)
} }
state := &canvasBackendState{ state := &canvasBackendState{
images: renderCache[*EncodedImage, image.Image]{limit: imageCacheLimit}, resources: r.Reader.resources,
images: *resourceCache[*EncodedImage, image.Image](r.Reader.resources, imageCacheLimit),
} }
state.resetFonts() state.resetFonts()
r.backendStates[key] = state r.backendStates[key] = state
@@ -150,9 +189,10 @@ func (r *Renderer) canvasState() *canvasBackendState {
// resetFonts 清除Canvas字体及字形缓存,保留编码图片适配对象 // resetFonts 清除Canvas字体及字形缓存,保留编码图片适配对象
func (s *canvasBackendState) resetFonts() { func (s *canvasBackendState) resetFonts() {
s.textGlyphPathCache.clear()
s.fontMap = make(map[string]*canvas.FontFamily) s.fontMap = make(map[string]*canvas.FontFamily)
s.svgFontCache = make(map[*canvas.Font]SVGFont) s.svgFontCache = make(map[*canvas.Font]SVGFont)
s.textGlyphPathCache = renderCache[textGlyphPathCacheKey, textGlyphPathCacheValue]{limit: 16 << 20} s.textGlyphPathCache = *resourceCache[textGlyphPathCacheKey, textGlyphPathCacheValue](s.resources, 16<<20)
s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner) s.fontOutlines = make(map[*canvas.Font]*basefont.Outliner)
} }
@@ -189,14 +229,14 @@ func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
// 返回: *canvas.Path 规范化填充轮廓 // 返回: *canvas.Path 规范化填充轮廓
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path { func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
if len(path.Data()) < 256 { if len(path.Data()) < 256 {
return path.Settle(rule) return settleCanvasPath(path, rule)
} }
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule} key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
cache := r.canvasSettledPaths() cache := r.canvasSettledPaths()
if result, ok := cache.get(key); ok { if result, ok := cache.get(key); ok {
return result.Copy() return result.Copy()
} }
result := path.Settle(rule) result := settleCanvasPath(path, rule)
if cost := len(result.Data())*8 + 256; cost <= cache.limit { if cost := len(result.Data())*8 + 256; cost <= cache.limit {
cache.put(key, result.Copy(), cost) cache.put(key, result.Copy(), cost)
} }
+88 -2
View File
@@ -20,6 +20,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/binary"
"fmt" "fmt"
"html" "html"
"image" "image"
@@ -59,6 +60,16 @@ type svgImageKey struct {
size image.Point size image.Point
} }
// svgResourceCacheKey 区分共享的图片编码缓存
type svgResourceCacheKey struct{}
// svgImageResourceKey 按像素、压缩策略及目标尺寸区分编码结果
type svgImageResourceKey struct {
digest [32]byte
options CompressionOptions
target image.Point
}
// svgObjectGroup 记录编辑分组的稳定路径,不把底纹内部绘制当作成员 // svgObjectGroup 记录编辑分组的稳定路径,不把底纹内部绘制当作成员
type svgObjectGroup struct { type svgObjectGroup struct {
path string path string
@@ -66,6 +77,59 @@ type svgObjectGroup struct {
composite bool composite bool
} }
// svgImageResources 获取派生渲染器共享的图片编码缓存
// 返回: *renderCache 图片编码缓存
func (r *Renderer) svgImageResources() *renderCache[svgImageResourceKey, SVGImage] {
key := svgResourceCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[svgImageResourceKey, SVGImage])
}
cache := resourceCache[svgImageResourceKey, SVGImage](r.Reader.resources, 4<<20)
r.sharedBackendStates[key] = cache
return cache
}
// svgPixelDigest 对标准像素缓冲计算完整摘要,不展开延迟图像
// 入参: ctx 取消上下文, img 原始图像, limit 可参与缓存的像素字节上限
// 返回: [32]byte 像素摘要, bool 是否为支持的像素布局
func svgPixelDigest(ctx context.Context, img image.Image, limit int) ([32]byte, bool) {
var pixels []byte
var stride, kind int
switch source := img.(type) {
case *image.NRGBA:
pixels, stride, kind = source.Pix, source.Stride, 1
case *image.NRGBA64:
pixels, stride, kind = source.Pix, source.Stride, 2
case *image.RGBA:
pixels, stride, kind = source.Pix, source.Stride, 3
case *image.RGBA64:
pixels, stride, kind = source.Pix, source.Stride, 4
default:
return [32]byte{}, false
}
if len(pixels) > limit {
return [32]byte{}, false
}
bounds := img.Bounds()
var header [48]byte
for i, value := range [...]int{kind, stride, bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y} {
binary.LittleEndian.PutUint64(header[i*8:], uint64(value))
}
hash := sha256.New()
hash.Write(header[:])
for len(pixels) > 0 {
if ctx.Err() != nil {
return [32]byte{}, false
}
count := min(len(pixels), 256<<10)
hash.Write(pixels[:count])
pixels = pixels[count:]
}
var digest [32]byte
hash.Sum(digest[:0])
return digest, true
}
// RenderImage 按输出策略编码图片,分离资源时保持尺寸和变换 // RenderImage 按输出策略编码图片,分离资源时保持尺寸和变换
// 入参: img 图片对象, m 变换矩阵 // 入参: img 图片对象, m 变换矩阵
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) { func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
@@ -134,11 +198,33 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
} }
} }
// imageResource 保留原编码或压缩已解码像素,先降采样再编码并响应取消 // imageResource 复用相同像素的编码结果,不持有源图像或改变输出策略
// 优化失败时回退原图PNG,取消时终止,不改变SVG中的图片尺寸和变换
// 入参: ctx 取消上下文, img 原图, target 压缩像素需求 // 入参: ctx 取消上下文, img 原图, target 压缩像素需求
// 返回: SVGImage 图片资源, error 编码或取消错误 // 返回: SVGImage 图片资源, error 编码或取消错误
func (s *svgResourceRenderer) imageResource(ctx context.Context, img image.Image, target image.Point) (SVGImage, error) { func (s *svgResourceRenderer) imageResource(ctx context.Context, img image.Image, target image.Point) (SVGImage, error) {
if err := ctx.Err(); err != nil {
return SVGImage{}, err
}
if s.renderer != nil {
cache := s.renderer.svgImageResources()
if digest, ok := svgPixelDigest(ctx, img, cache.limit); ok {
key := svgImageResourceKey{digest: digest, options: s.renderer.Compression, target: target}
result, err := cache.load(ctx, key, func() (SVGImage, int, error) {
result, err := s.encodeImageResource(ctx, img, target)
return result, cap(result.Data) + 256, err
})
result.Data = bytes.Clone(result.Data)
return result, err
}
}
return s.encodeImageResource(ctx, img, target)
}
// encodeImageResource 保留原编码或压缩已解码像素,先降采样再编码并响应取消
// 优化失败时回退原图PNG,取消时终止,不改变SVG中的图片尺寸和变换
// 入参: ctx 取消上下文, img 原图, target 压缩像素需求
// 返回: SVGImage 图片资源, error 编码或取消错误
func (s *svgResourceRenderer) encodeImageResource(ctx context.Context, img image.Image, target image.Point) (SVGImage, error) {
options := CompressionOptions{} options := CompressionOptions{}
if s.renderer != nil { if s.renderer != nil {
options = s.renderer.Compression options = s.renderer.Compression
+4 -4
View File
@@ -170,7 +170,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} }
m := canvas.Matrix{{boundaryCTM.a, -boundaryCTM.c, boundaryCTM.e}, {-boundaryCTM.b, boundaryCTM.d, pageH - boundaryCTM.f}} m := canvas.Matrix{{boundaryCTM.a, -boundaryCTM.c, boundaryCTM.e}, {-boundaryCTM.b, boundaryCTM.d, pageH - boundaryCTM.f}}
path := canvas.Rectangle(box.W, box.H).Translate(0, -box.H).Transform(m) path := canvas.Rectangle(box.W, box.H).Translate(0, -box.H).Transform(m)
box = textPathBox(applyClipPath(path, clipPath), pageH) box = textPathBox(r.applyCachedClipPath(path, clipPath), pageH)
for i := range textRun.Boxes { for i := range textRun.Boxes {
textRun.Boxes[i] = box textRun.Boxes[i] = box
} }
@@ -292,7 +292,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
m = m.Mul(glyphMatrix) m = m.Mul(glyphMatrix)
} }
path := glyphPath.Copy().Transform(m.Scale(scaleX, 1)) path := glyphPath.Copy().Transform(m.Scale(scaleX, 1))
box := textPathBox(applyClipPath(path, clipPath), pageH) box := textPathBox(r.applyCachedClipPath(path, clipPath), pageH)
for j := spans[i][0]; j < spans[i][1]; j++ { for j := spans[i][0]; j < spans[i][1]; j++ {
textRun.Boxes[textPos+j] = unionTextBox(textRun.Boxes[textPos+j], box) textRun.Boxes[textPos+j] = unionTextBox(textRun.Boxes[textPos+j], box)
} }
@@ -321,7 +321,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView) r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView)
} }
path = r.strokeDashedCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, strokeStyle.dashOffset, strokeStyle.dashPattern, false) path = r.strokeDashedCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, strokeStyle.dashOffset, strokeStyle.dashPattern, false)
path = applyClipPath(path, strokeClip) path = r.applyCachedClipPath(path, strokeClip)
if strokeStyle.strokePattern != nil { if strokeStyle.strokePattern != nil {
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM) r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
} else { } else {
@@ -378,7 +378,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if useTextMatrix { if useTextMatrix {
textTransform = textTransform.Mul(glyphMatrix) textTransform = textTransform.Mul(glyphMatrix)
} }
glyphPath = applyClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), glyphClip) glyphPath = r.applyCachedClipPath(glyphPath.Copy().Transform(textTransform.Scale(scaleX, 1)), glyphClip)
if fillPattern != nil { if fillPattern != nil {
r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM) r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM)
} else { } else {
+17 -6
View File
@@ -76,7 +76,7 @@ type fileOutput struct {
type editorSnapshotArchive struct { type editorSnapshotArchive struct {
files []*zip.File files []*zip.File
archive weak.Pointer[zip.Reader] archive weak.Pointer[zip.Reader]
retained *zip.Reader retained *renderCache[int, *zip.Reader]
} }
// editorSnapshotBuffer 分块保存压缩数据,写入完成后支持并发随机读取 // editorSnapshotBuffer 分块保存压缩数据,写入完成后支持并发随机读取
@@ -121,7 +121,7 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
return nil, err return nil, err
} }
defer reader.Close() defer reader.Close()
next := &Reader{files: make(map[string][]byte, len(reader.files)), encryption: reader.encryption, documentIndex: reader.documentIndex, selectedVersion: cloneEditorData(reader.selectedVersion)} next := &Reader{files: make(map[string][]byte, len(reader.files)), encryption: reader.encryption, documentIndex: reader.documentIndex, selectedVersion: cloneEditorData(reader.selectedVersion), resources: e.runtimeResources}
if reader.encryption != nil { if reader.encryption != nil {
next.encryption = &encryptionState{info: cloneEditorData(reader.encryption.info), options: cloneEncryptionOptions(reader.encryption.options)} next.encryption = &encryptionState{info: cloneEditorData(reader.encryption.info), options: cloneEncryptionOptions(reader.encryption.options)}
} }
@@ -157,6 +157,11 @@ func (e *Editor) copySnapshotArchive(ctx context.Context, source *zip.Reader) (*
return nil, err return nil, err
} }
if cached := e.snapshotArchive; cached != nil && slices.Equal(cached.files, source.File) { if cached := e.snapshotArchive; cached != nil && slices.Equal(cached.files, source.File) {
if cached.retained != nil {
if archive, ok := cached.retained.get(0); ok {
return archive, nil
}
}
if archive := cached.archive.Value(); archive != nil { if archive := cached.archive.Value(); archive != nil {
return archive, nil return archive, nil
} }
@@ -183,8 +188,14 @@ func (e *Editor) copySnapshotArchive(ctx context.Context, source *zip.Reader) (*
return nil, err return nil, err
} }
cached := &editorSnapshotArchive{files: slices.Clone(source.File), archive: weak.Make(reader)} cached := &editorSnapshotArchive{files: slices.Clone(source.File), archive: weak.Make(reader)}
if len(data.chunks) <= editorSnapshotArchiveLimit/editorSnapshotChunkSize { limit := int64(editorSnapshotArchiveLimit)
cached.retained = reader if e.runtimeResources != nil {
limit = e.runtimeResources.options.SnapshotBytes
}
if int64(len(data.chunks)) <= limit/editorSnapshotChunkSize {
cached.retained = resourceCache[int, *zip.Reader](e.runtimeResources, editorSnapshotArchiveLimit)
cached.retained.limit = int(min(limit, int64(int(^uint(0)>>1)-1)))
cached.retained.put(0, reader, len(data.chunks)*editorSnapshotChunkSize)
} }
e.snapshotArchive = cached e.snapshotArchive = cached
return reader, nil return reader, nil
@@ -530,7 +541,7 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
if e.source != nil { if e.source != nil {
return e.sourceReader(ctx, progress) return e.sourceReader(ctx, progress)
} }
r := &Reader{files: make(map[string][]byte), encryption: e.encryption} r := &Reader{files: make(map[string][]byte), encryption: e.encryption, resources: e.runtimeResources}
if err := e.writeParts(ctx, func(name string, data []byte, _ bool) error { if err := e.writeParts(ctx, func(name string, data []byte, _ bool) error {
r.files[name] = data r.files[name] = data
return nil return nil
@@ -562,7 +573,7 @@ func (e *Editor) resourceReader() (*Reader, error) {
if err := e.validate(); err != nil { if err := e.validate(); err != nil {
return nil, err return nil, err
} }
r := &Reader{files: make(map[string][]byte)} r := &Reader{files: make(map[string][]byte), resources: e.runtimeResources}
if err := e.writePartsWithPages(ctx, func(name string, data []byte, _ bool) error { if err := e.writePartsWithPages(ctx, func(name string, data []byte, _ bool) error {
r.files[name] = data r.files[name] = data
return nil return nil