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
+633 -125

No files matched your search

+10 -3
View File
@@ -419,6 +419,7 @@ type outputContext struct {
// businessEngine 独立文档、编辑、剪贴板及操作上下文
type businessEngine struct {
resources *ofdgo.Resources
currentSession *Session
currentEditor *ofdgo.Editor
saveSnapshot *ofdgo.Editor
@@ -1396,7 +1397,7 @@ func (e *businessEngine) convertPDFDocument(args []platformValue) (any, error) {
return nil, err
}
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() {
options.Password, err = e.bytesFromJS(args[2])
if err != nil {
@@ -1450,7 +1451,8 @@ func (e *businessEngine) convertFile(args []platformValue) (any, error) {
return nil, err
}
options := ofdgo.ConvertOptions{
Format: settings.Get("format").String(), PageRange: settings.Get("pages").String(),
Resources: e.resources,
Format: settings.Get("format").String(), PageRange: settings.Get("pages").String(),
Pool: e.conversionPool,
SkipUnchanged: true,
Backends: &backends,
@@ -1608,7 +1610,7 @@ func (e *businessEngine) openDocument(args []platformValue) (any, error) {
return nil, err
}
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() {
credentials, err := e.credentialsFromJS(args[3])
if err != nil {
@@ -4047,6 +4049,7 @@ func (e *businessEngine) previewText(args []platformValue) (any, error) {
return nil, err
}
editor = ofdgo.NewEditor()
editor.SetResources(e.resources)
fontID, err = editor.AddFont(FontFile{Data: data}, 0)
if err != nil {
return nil, err
@@ -5568,6 +5571,7 @@ func (e *businessEngine) loadImport(args []platformValue) (any, error) {
}
}()
var options []ofdgo.ReaderOption
options = append(options, ofdgo.WithResources(e.resources))
if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() {
credentials, err := e.credentialsFromJS(args[1])
if err != nil {
@@ -5685,6 +5689,7 @@ func (e *businessEngine) importImages(args []platformValue) (any, error) {
editor := e.currentEditor
if at < 0 {
editor = ofdgo.NewEditor()
editor.SetResources(e.resources)
editor.Info.Title = args[3].String()
} else if editor == nil {
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) {
e.clearImport()
editor := ofdgo.NewEditor()
editor.SetResources(e.resources)
editor.Info.Title = args[0].String()
if _, err := editor.AddPage(args[1].Float(), args[2].Float()); err != nil {
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})
}
editor := ofdgo.NewEditor()
editor.SetResources(e.resources)
if _, err := editor.AddPage(seal.Width, seal.Height); err != nil {
return nil, err
}
+10
View File
@@ -70,6 +70,8 @@ type NativeOutputTarget struct {
// hooks应在首次Call前配置,Resolve返回的资源需保持有效直到相应会话关闭
type NativeEngine struct {
businessEngine
// Resources 协调会话的计算与缓存,需在首次Call前配置
Resources *ofdgo.Resources
// ConversionPool 协调不同会话的批量转换资源
ConversionPool *ofdgo.ConversionPool
// 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.resolveInput = e.input
e.conversionPool = e.ConversionPool
e.resources = e.Resources
})
if ctx == nil {
ctx = context.Background()
@@ -163,6 +166,13 @@ func (e *NativeEngine) Call(ctx context.Context, name string, args []any, emit f
case <-ctx.Done():
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 {
case e.gate <- struct{}{}:
+2 -1
View File
@@ -39,7 +39,8 @@ type platformValue = js.Value
// RunWASM 注册浏览器WASM接口并阻塞运行
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{
"ofdgoDetectFormat": true,
"ofdgoExportVersion": true,
+12 -3
View File
@@ -51,6 +51,7 @@ type Host struct {
engines map[string]*webui.NativeEngine
engineStops map[string]func() bool
conversionPool *ofdgo.ConversionPool
resources *ofdgo.Resources
cancels map[string]context.CancelFunc
batchResults map[string]any
fontOnce sync.Once
@@ -79,11 +80,16 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
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.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 {
files.close()
return err
}
h.conversionPool = h.resources.ConversionPool()
defer h.close()
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"} {
@@ -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)
if memory := conversionMemory; memory != nil {
available := memory()
parallelism = max(1, min(parallelism, int(available/(256<<20))))
cacheBytes = max(32<<20, min(256<<20, available/16))
cacheBytes = max(32<<20, available/32)
}
engine.RenderParallelism = parallelism
engine.ConversionPool = h.conversionPool
engine.Resources = h.resources
engine.OpenWriter = h.openWriter
engine.ReserveOutput = h.reserveOutput
engine.CleanupOutput = h.cleanupOutput
@@ -798,6 +804,9 @@ func (h *Host) close() {
for _, result := range results {
h.discard(result)
}
if h.resources != nil {
h.resources.Clear()
}
_ = h.files.close()
}
+4 -1
View File
@@ -290,6 +290,9 @@ func NewReader(input *Input, options ...ReaderOption) (*Reader, error) {
// 入参: reader 阅读器, opts 渲染选项
// 返回: *Renderer 渲染器实例
func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
if reader.resources == nil {
reader.resources, _ = NewResources(ResourceOptions{})
}
r := &Renderer{
Reader: reader,
DPI: 300.0,
@@ -297,7 +300,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
DrawParams: reader.drawParamCache,
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
templatePageCache: make(map[string]*PageContent),
imageCache: &renderCache[string, image.Image]{limit: imageCacheLimit},
imageCache: resourceCache[string, image.Image](reader.resources, imageCacheLimit),
backends: defaultRenderBackends(),
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 输出配置
// 返回: 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 {
return 0, err
}
@@ -125,6 +126,7 @@ func (e *Editor) WritePagesTo(ctx context.Context, writer io.Writer, indexes []i
// 入参: ctx 取消上下文, options 输出配置
// 返回: *Editor 输出快照, error 配置或资源错误
func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Editor, error) {
ctx = e.runtimeResources.context(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
+9 -11
View File
@@ -20,16 +20,12 @@ import (
"crypto/sha256"
"image"
"io"
"strconv"
"sync"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
)
// outputImageBatchLimit 限制并行任务持有的原始编码,超限图片单独处理
const outputImageBatchLimit = 16 << 20
// outputImageWorkingSetLimit 限制64位并行任务的像素工作量总估算,32位使用64MiB,超限图片单独处理
// outputImageWorkingSetLimit 指定未提供任务预算时的图像成本估算上限
const outputImageWorkingSetLimit = 256 << 20
// outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调
@@ -82,20 +78,22 @@ func (e *Editor) outputImageOptions(key string) (CompressionOptions, image.Point
// 入参: keys 资源路径, load 串行读取方法,空值表示跳过, results 候选缓存, sources 原始资源摘要,可为空
// 返回: 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 {
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 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))))
encodedLimit = limit
}
}
if strconv.IntSize == 32 {
limit = 64 << 20
}
base, cancel := context.WithCancel(e.output.ctx)
ctx := outputImageContext{Context: base, parent: e.output.ctx}
var group sync.WaitGroup
+14 -5
View File
@@ -52,6 +52,7 @@ type ConvertProgress struct {
// PageRange使用一基页码表达式,与Pages互斥,空字符串表示全部页面
// SkipUnchanged仅在未压缩、源格式与目标相同且未选页时不写出数据
// Pool可由多个独立转换共享,nil时不限制文件间并发
// Resources提供共享计算与缓存策略,显式Pool优先控制计算调度
// 整份PDF同格式压缩直接重写PDF对象,其余PDF先转换为OFD对象再输出
// RendererOptions同时用于PDF局部合成和输出,在PDF.RendererOptions之后应用
// Backends统一配置转换与输出,优先于PDF和RendererOptions中的后端设置
@@ -60,6 +61,7 @@ type ConvertProgress struct {
// PageOutput仅用于逐页格式,同步调用export写出一页并自行处理提交或回滚;此时output可为nil
// 未提供PageOutput时,逐页格式打包ZIP;所有回调返回错误均会停止转换
type ConvertOptions struct {
Resources *Resources
Input string
Format string
Pages []int
@@ -145,6 +147,13 @@ func OutputFormats() []OutputFormat {
// 入参: ctx 取消上下文, input 输入源, output 输出流, options 转换选项
// 返回: ConvertReport 转换报告, 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 err = options.Pool.acquire(ctx, false, 0); err != nil {
return report, err
@@ -534,7 +543,7 @@ func (p *ConversionPool) acquire(ctx context.Context, image bool, size int64) er
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)
allowed = p.images < p.options.Parallelism && (p.images == 0 || limit < 0 || size <= limit-p.memory)
}
if allowed {
if image {
@@ -584,15 +593,15 @@ func (p *ConversionPool) release(image bool, size int64) {
}
// conversionWorkers 按当前文件数量分配内部计算任务
// 入参: ctx 转换上下文, fallback 非共享模式并行上限
// 入参: ctx 转换上下文
// 返回: int 内部并行数
func conversionWorkers(ctx context.Context, fallback int) int {
func conversionWorkers(ctx context.Context) int {
p, _ := ctx.Value(conversionPoolKey{}).(*ConversionPool)
if p == nil {
return min(fallback, runtime.GOMAXPROCS(0))
return runtime.GOMAXPROCS(0)
}
p.mu.Lock()
active := p.active
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)
}
// ReaderOption 阅读器的凭据与密码提供者选项
// ReaderOption 阅读器的凭据、密码提供者与资源选项
type ReaderOption func(*readerOptions)
// EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码
@@ -92,6 +92,7 @@ type EncryptionInfo struct {
// readerOptions 保存打开文档时的会话参数
type readerOptions struct {
resources *Resources
credentials []Credentials
provider CryptoProvider
}
+1
View File
@@ -113,6 +113,7 @@ func (r *Reader) open(options []ReaderOption) error {
for _, option := range options {
option(&settings)
}
r.resources = settings.resources
if settings.provider == nil {
settings.provider = GMCryptoProvider{}
}
+29 -2
View File
@@ -50,6 +50,7 @@ type Editor struct {
Info DocInfo
pages []PageContent
resources []editorResource
runtimeResources *Resources
fonts map[string]*FontResource
fontRegistration editorFontRegistration
fontDirs []string
@@ -154,11 +155,13 @@ func (e *Editor) Backends() RenderBackends {
// NewEditor 创建空白文档,保存前至少添加一页
// 返回: *Editor 文档编辑器
func NewEditor() *Editor {
resources, _ := NewResources(ResourceOptions{})
var id [16]byte
rand.Read(id[:])
id[6] = id[6]&0x0f | 0x40
id[8] = id[8]&0x3f | 0x80
return &Editor{
runtimeResources: resources,
Info: DocInfo{
DocID: hex.EncodeToString(id[:]),
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 获取当前页数
// 返回: int 页数
func (e *Editor) PageCount() int {
@@ -773,7 +800,7 @@ func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) e
// 返回: *Renderer 度量器
func (e *Editor) newRenderer(reader *Reader) *Renderer {
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.fontSourcesCache = e.fontSourcesCache.child()
@@ -1325,7 +1352,7 @@ func (v *editorValidation) creationPath(value string) error {
// 返回: *renderCache[string, bool] 校验缓存
func (e *Editor) pathValidationCache() *renderCache[string, bool] {
if e.validatedPaths == nil {
e.validatedPaths = &renderCache[string, bool]{limit: 8 << 20}
e.validatedPaths = resourceCache[string, bool](e.runtimeResources, 8<<20)
}
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.saveTarget = e.saveTarget
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
next.SetResources(e.runtimeResources)
next.validatedPaths = e.validatedPaths
*e = *next
}
+1
View File
@@ -907,6 +907,7 @@ func (e *Editor) restoreResourceSnapshot(state Editor) {
state.clearFontResourceCache()
}
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.saveTarget = e.saveTarget
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.backends, e.fontDirs, e.fontFS = before.backends, before.fontDirs, before.fontFS
e.SetResources(before.runtimeResources)
e.fontSourcesCache = before.fontSourcesCache
if e.sameResourceView(&before) {
e.fontRenderer, e.fontMetrics = before.fontRenderer, before.fontMetrics
@@ -168,6 +169,7 @@ func (e *Editor) restoreTransaction(state Editor) {
state.clearFontResourceCache()
}
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.SetResources(e.runtimeResources)
state.fontSourcesCache = e.fontSourcesCache
state.validatedPaths = e.validatedPaths
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.runtimeResources = r.resources
e.encryption = r.encryption
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)}
+1 -1
View File
@@ -817,7 +817,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
files = make(map[string][]byte)
}
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 {
return nil, err
}
+17 -5
View File
@@ -269,23 +269,35 @@ func (r *Renderer) fontDigest(data []byte) [32]byte {
// resetFontCache 重置字体解析及字形缓存,保留图片和非字体后端状态
func (r *Renderer) resetFontCache() {
r.fontSourcesCache = newFontSourceCache()
if r.fontSourcesCache != nil {
r.fontSourcesCache.collections.clear()
}
r.fontSourcesCache = newFontSourceCache(r.Reader.resources)
r.resetFontBackendCache()
}
// 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.preparedFonts = make(map[string]*PreparedFont)
r.fontDigests = &renderCache[fontDataKey, [32]byte]{limit: 4 << 20}
r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 16 << 20}
r.fontDigests = resourceCache[fontDataKey, [32]byte](r.Reader.resources, 4<<20)
r.fontPreparations = resourceCache[[32]byte, PreparedFont](r.Reader.resources, 16<<20)
r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult)
if r.fontSourcesCache != nil {
r.fontSourcesCache.fallback = ResolvedFont{}
r.fontSourcesCache.fallbackRead = false
}
r.fontMetrics = &renderCache[[32]byte, FontMetrics]{limit: 32 << 20}
r.glyphOutlines = &renderCache[glyphOutlineKey, GeometryPath]{limit: 16 << 20}
r.fontMetrics = resourceCache[[32]byte, FontMetrics](r.Reader.resources, 32<<20)
r.glyphOutlines = resourceCache[glyphOutlineKey, GeometryPath](r.Reader.resources, 16<<20)
for _, state := range r.backendStates {
if fonts, ok := state.(interface{ resetFonts() }); ok {
fonts.resetFonts()
+4 -3
View File
@@ -79,15 +79,16 @@ type fontSourceData struct {
}
// newFontSourceCache 创建独立字体来源缓存
// 入参: resources 共享资源策略
// 返回: *fontSourceCache 字体来源缓存
func newFontSourceCache() *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}}
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: resourceCache[fontSourceKey, []byte](resources, 32<<20)}
}
// 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.collections = c.collections
return cache
+10 -2
View File
@@ -41,7 +41,9 @@ var ErrPDFPassword = pdfgo.ErrPassword
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
// Halftones提供只读设备命名网屏,所属阅读器由调用方维护,连续色调输出不网屏化
// Compression配置转换输出,有损模式按图片实际尺寸降采样,原始及无损模式保持完整采样
// Resources用于导入、局部合成与返回编辑器的共享资源预算
type PDFImportOptions struct {
Resources *Resources
Password []byte
PasswordUTF8 bool
Backends *RenderBackends
@@ -68,6 +70,7 @@ type PDFImportReport struct {
// pdfImporter 保存单次转换的对象与资源状态
type pdfImporter struct {
resources *Resources
ctx context.Context
reader *pdfgo.Reader
editor *Editor
@@ -207,6 +210,10 @@ func (o pdfObjectOrigin) merge(other pdfObjectOrigin) pdfObjectOrigin {
// 入参: ctx 取消上下文, input 输入源, options 转换选项
// 返回: *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 {
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 {
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 {
WithRenderBackends(*options.Backends)(renderer)
}
@@ -234,10 +241,11 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
return nil, PDFImportReport{}, err
}
editor := NewEditor()
editor.runtimeResources = options.Resources
editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources()
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}
var diagnostics []pdfgo.Diagnostic
var readWarning func(pdfgo.Diagnostic)
+3 -1
View File
@@ -246,6 +246,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
}
matrix, width, height := codecpdf.PageMatrix(page)
editor := NewEditor()
editor.SetResources(p.resources)
editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...)
@@ -255,7 +256,8 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
return Box{}, err
}
local := pdfImporter{
ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer,
resources: p.resources,
ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer,
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
+4 -3
View File
@@ -126,6 +126,7 @@ type pdfCompositeImageColor struct {
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct {
resources *Resources
geometry *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]
clips *renderCache[[32]byte, GeometryPath]
bounds map[pdfCompositeKey]Box
@@ -157,7 +158,7 @@ type pdfCompositeScratch struct {
// 返回: *pdfCompositeCache 当前页面缓存
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
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
}
@@ -166,7 +167,7 @@ func (p *pdfImporter) compositingCache() *pdfCompositeCache {
// 返回: *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] 单页场景缓存
func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] {
if c.geometry == nil {
c.geometry = &renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]{limit: 64 << 20}
c.geometry = resourceCache[pdfCompositeSceneKey, pdfCompositeGeometry](c.resources, 64<<20)
}
return c.geometry
}
@@ -175,7 +176,7 @@ func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, p
// 返回: *renderCache[[32]byte, GeometryPath] 裁剪缓存
func (c *pdfCompositeCache) clipCache() *renderCache[[32]byte, GeometryPath] {
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
}
+1 -1
View File
@@ -108,7 +108,7 @@ func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
return true, nil
}
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
defer cache.reset()
+1 -1
View File
@@ -419,7 +419,7 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
return pdfClipPath{}, err
}
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 {
return cached, nil
+3 -2
View File
@@ -383,6 +383,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
return nil, box, err
}
editor := NewEditor()
editor.SetResources(p.resources)
editor.SetRenderBackends(p.editor.Backends())
editor.SetFontDirs(p.editor.fontDirs...)
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)
}
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 {
return nil, box, err
}
+1 -1
View File
@@ -136,7 +136,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
}
if cacheable {
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) {
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)
}
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}
if source.PaintType == 2 {
+2 -2
View File
@@ -887,7 +887,7 @@ func (p *pdfImporter) linearMeshTriangles(mesh *pdfgo.MeshGradient, index int) (
}
cache := p.compositingCache()
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}
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
}
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)
if err != nil {
return nil, err
+8 -4
View File
@@ -37,6 +37,7 @@ type Reader struct {
OFD *OFD
RootDir string
ResMap map[string]string
resources *Resources
resourcesRead map[string]bool
resourceFiles map[string]string
imageCatalog []ImageInfo
@@ -113,7 +114,7 @@ func (r *Reader) Fork(ctx context.Context) (*Reader, error) {
return nil, err
}
if r.pageContents == nil {
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20}
r.pageContents = resourceCache[pageContentKey, PageContent](r.resources, 16<<20)
}
next := *r
next.Closer = nil
@@ -210,7 +211,7 @@ func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error
if index < 0 || index >= r.DocumentCount() {
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 {
return nil, err
}
@@ -762,6 +763,9 @@ func (r *Reader) initRoot() error {
// 入参: ctx 取消上下文
// 返回: error 索引、读取、解析或取消错误
func (r *Reader) initRootContext(ctx context.Context) error {
if r.resources == nil {
r.resources, _ = NewResources(ResourceOptions{})
}
view := *r
if err := view.indexPackageContext(ctx); err != nil {
return err
@@ -780,7 +784,7 @@ func (r *Reader) initRootContext(ctx context.Context) error {
}
r.fileIndex, r.fileNames = view.fileIndex, view.fileNames
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.doc, r.versionInfo = nil, nil
@@ -1100,7 +1104,7 @@ func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, templ
return nil, err
}
if r.pageContents == nil {
r.pageContents = &renderCache[pageContentKey, PageContent]{limit: 16 << 20}
r.pageContents = resourceCache[pageContentKey, PageContent](r.resources, 16<<20)
}
var content PageContent
if len(data) > (r.pageContents.limit/4-4096)/16 {
+245 -24
View File
@@ -21,13 +21,49 @@ import (
"image"
"reflect"
"sync"
"time"
"weak"
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 按估算字节数限制派生渲染数据,淘汰不修改源资源
type renderCache[K comparable, V any] struct {
mu sync.Mutex
resources *Resources
limit, used int
entries map[K]*list.Element
order list.List
@@ -43,19 +79,178 @@ type renderCacheLoad[V any] struct {
// renderCacheEntry 保存缓存值及内存成本
type renderCacheEntry[K comparable, V any] struct {
key K
value V
cost int
key K
value V
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 读取缓存并更新使用顺序
// 入参: key 缓存键
// 返回: V 缓存值, bool 是否命中
func (c *renderCache[K, V]) get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.mutex().Lock()
defer c.mutex().Unlock()
if item := c.entries[key]; item != nil {
c.order.MoveToFront(item)
c.touch(item)
return item.Value.(renderCacheEntry[K, V]).value, true
}
var zero V
@@ -65,12 +260,10 @@ func (c *renderCache[K, V]) get(key K) (V, bool) {
// put 缓存预算内的派生数据,超大单项不进入缓存
// 入参: key 缓存键, value 缓存值, cost 字节成本
func (c *renderCache[K, V]) put(key K, value V, cost int) {
c.mu.Lock()
defer c.mu.Unlock()
c.mutex().Lock()
defer c.mutex().Unlock()
if item := c.entries[key]; item != nil {
c.used -= item.Value.(renderCacheEntry[K, V]).cost
c.order.Remove(item)
delete(c.entries, key)
c.remove(item)
}
if cost <= 0 || cost > c.limit {
return
@@ -78,15 +271,43 @@ func (c *renderCache[K, V]) put(key K, value V, cost int) {
if c.entries == nil {
c.entries = make(map[K]*list.Element)
}
for c.used+cost > c.limit {
for cost > c.limit-c.used {
item := c.order.Back()
entry := item.Value.(renderCacheEntry[K, V])
c.used -= entry.cost
delete(c.entries, entry.key)
c.order.Remove(item)
c.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
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 复用缓存并合并并发请求,等待取消不影响正在读取的任务
@@ -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 {
return zero, err
}
c.mu.Lock()
c.mutex().Lock()
if item := c.entries[key]; item != nil {
c.order.MoveToFront(item)
c.touch(item)
value := item.Value.(renderCacheEntry[K, V]).value
c.mu.Unlock()
c.mutex().Unlock()
return value, nil
}
if pending := c.loading[key]; pending != nil {
c.mu.Unlock()
c.mutex().Unlock()
select {
case <-ctx.Done():
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")}
c.loading[key] = pending
c.mu.Unlock()
c.mutex().Unlock()
defer func() {
c.mu.Lock()
c.mutex().Lock()
delete(c.loading, key)
close(pending.done)
c.mu.Unlock()
c.mutex().Unlock()
}()
value, cost, err := read()
if err == nil {
+1 -1
View File
@@ -77,7 +77,7 @@ func newSemanticCompiler(r *Renderer) (*semanticCompiler, error) {
if err != nil {
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 转换光栅曲线,仅在输出边界展开椭圆弧
+6 -4
View File
@@ -34,8 +34,9 @@ type stampContentKey struct {
// stampContentState 保存有界的共享签章编译结果及配置
type stampContentState struct {
config RenderBackends
cache renderCache[stampContentKey, []RasterCommand]
resources *Resources
config RenderBackends
cache renderCache[stampContentKey, []RasterCommand]
}
// stampContentStateKey 隔离渲染器私有的共享签章数据
@@ -52,7 +53,8 @@ type patternCellKey struct {
// 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 复用相同签章的解码、嵌套文档编译及局部裁剪,不缓存放置位置
@@ -74,7 +76,7 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
config := r.backends
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) {
state = &stampContentState{config: config}
state = &stampContentState{config: config, resources: r.Reader.resources}
state.resetFonts()
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.fontMetrics = r.fontMetrics
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
}
@@ -108,8 +113,9 @@ func (r *Renderer) deviceLineWidth(width float64) float64 {
// ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置
func (r *Renderer) ClearCache() {
r.imageCache.clear()
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.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))
}
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))
}
}
@@ -164,7 +164,7 @@ func (r *Renderer) buildClipAreaPath(obj PathObject, drawParam string, parent Ma
}
result = paths.Merge()
if obj.Rule == "Even-Odd" {
result = result.Settle(canvas.EvenOdd)
result = settleCanvasPath(result, canvas.EvenOdd)
}
}
if stroke {
@@ -255,12 +255,12 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
return result
}
if result, ok := intersectLinearCanvasContours(parent, current); ok {
return result.Settle(canvas.NonZero)
return settleCanvasPath(result, canvas.NonZero)
}
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 逐边裁剪单个凸多边形,避免仿射矩形共边进入曲线扫线
@@ -396,7 +396,7 @@ func unionClipPath(left, right *canvas.Path) *canvas.Path {
return bounds.ToPath()
}
}
return left.Or(right)
return combineCanvasPaths(left, right, "or")
}
// applyClipPath 应用裁剪路径
@@ -432,12 +432,12 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
return result
}
if result, ok := intersectLinearCanvasContours(path, clip); ok {
return result.Settle(canvas.NonZero)
return settleCanvasPath(result, canvas.NonZero)
}
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 排除与裁剪边界分离的完整轮廓,保留相交轮廓的曲线和绕向
+45 -5
View File
@@ -20,10 +20,50 @@ import (
"math"
"slices"
"strconv"
"sync"
"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 在输出边界转换椭圆弧,其他曲线保持原样
// 入参: path 页面路径
// 返回: GeometryPath 贝塞尔路径, error 路径错误
@@ -108,7 +148,7 @@ func (CanvasBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, e
if evenOdd {
rule = canvas.EvenOdd
}
p = p.Settle(rule)
p = settleCanvasPath(p, rule)
if p.Empty() {
return nil, nil
}
@@ -132,11 +172,11 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
case GeometryIntersect:
result = applyClipPath(a, b)
case GeometryUnion:
result = a.Or(b)
result = combineCanvasPaths(a, b, "or")
case GeometrySubtract:
result = a.Not(b)
result = combineCanvasPaths(a, b, "not")
case GeometryXor:
result = a.Xor(b)
result = combineCanvasPaths(a, b, "xor")
default:
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)
p = p.Dash(options.DashOffset, options.Dashes...)
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
}
+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)
}
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))
}
}
+2 -2
View File
@@ -151,10 +151,10 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
}
start := buf.Len()
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() {
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
+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 {
outline := g.gradient.Mesh.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
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) {
p := path.Copy()
if ctx.FillRule == canvas.EvenOdd {
p = p.Settle(canvas.EvenOdd)
p = settleCanvasPath(p, canvas.EvenOdd)
}
p = p.Transform(view.Inv())
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()
}
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
}
+49 -9
View File
@@ -40,6 +40,9 @@ type canvasPathCacheKey struct{}
// canvasFillCacheKey 区分按填充规则规范化的共享路径缓存
type canvasFillCacheKey struct{}
// canvasDrawClipCacheKey 区分绘制轮廓的共享裁剪缓存
type canvasDrawClipCacheKey struct{}
// canvasOutlineCacheKey 区分子渲染器共享的字体轮廓解析缓存
type canvasOutlineCacheKey struct{}
@@ -57,6 +60,7 @@ type canvasPathKey struct {
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
type canvasBackendState struct {
resources *Resources
images renderCache[*EncodedImage, image.Image]
fontMap map[string]*canvas.FontFamily
svgFontCache map[*canvas.Font]SVGFont
@@ -74,7 +78,7 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path
if r.sharedBackendStates == nil {
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
return cache
}
@@ -89,7 +93,7 @@ func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path]
if r.sharedBackendStates == nil {
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
return cache
}
@@ -104,7 +108,7 @@ func (r *Renderer) canvasSettledPaths() *renderCache[canvasFillKey, *canvas.Path
if r.sharedBackendStates == nil {
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
return cache
}
@@ -116,11 +120,45 @@ func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.Font
if state, ok := r.sharedBackendStates[key]; ok {
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
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 获取子渲染器共享的字体轮廓解析缓存,限制字体及解析数据保留量
// 返回: *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 {
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
return cache
}
@@ -141,7 +179,8 @@ func (r *Renderer) canvasState() *canvasBackendState {
return 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()
r.backendStates[key] = state
@@ -150,9 +189,10 @@ func (r *Renderer) canvasState() *canvasBackendState {
// resetFonts 清除Canvas字体及字形缓存,保留编码图片适配对象
func (s *canvasBackendState) resetFonts() {
s.textGlyphPathCache.clear()
s.fontMap = make(map[string]*canvas.FontFamily)
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)
}
@@ -189,14 +229,14 @@ func canvasGeometryDigest(paths ...*canvas.Path) [32]byte {
// 返回: *canvas.Path 规范化填充轮廓
func (r *Renderer) settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path {
if len(path.Data()) < 256 {
return path.Settle(rule)
return settleCanvasPath(path, rule)
}
key := canvasFillKey{digest: canvasGeometryDigest(path), rule: rule}
cache := r.canvasSettledPaths()
if result, ok := cache.get(key); ok {
return result.Copy()
}
result := path.Settle(rule)
result := settleCanvasPath(path, rule)
if cost := len(result.Data())*8 + 256; cost <= cache.limit {
cache.put(key, result.Copy(), cost)
}
+88 -2
View File
@@ -20,6 +20,7 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"html"
"image"
@@ -59,6 +60,16 @@ type svgImageKey struct {
size image.Point
}
// svgResourceCacheKey 区分共享的图片编码缓存
type svgResourceCacheKey struct{}
// svgImageResourceKey 按像素、压缩策略及目标尺寸区分编码结果
type svgImageResourceKey struct {
digest [32]byte
options CompressionOptions
target image.Point
}
// svgObjectGroup 记录编辑分组的稳定路径,不把底纹内部绘制当作成员
type svgObjectGroup struct {
path string
@@ -66,6 +77,59 @@ type svgObjectGroup struct {
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 按输出策略编码图片,分离资源时保持尺寸和变换
// 入参: img 图片对象, m 变换矩阵
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 保留原编码或压缩已解码像素,先降采样再编码并响应取消
// 优化失败时回退原图PNG,取消时终止,不改变SVG中的图片尺寸和变换
// imageResource 复用相同像素的编码结果,不持有源图像或改变输出策略
// 入参: ctx 取消上下文, img 原图, target 压缩像素需求
// 返回: 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{}
if s.renderer != nil {
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}}
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 {
textRun.Boxes[i] = box
}
@@ -292,7 +292,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
m = m.Mul(glyphMatrix)
}
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++ {
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)
}
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 {
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
} else {
@@ -378,7 +378,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if useTextMatrix {
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 {
r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM)
} else {
+17 -6
View File
@@ -76,7 +76,7 @@ type fileOutput struct {
type editorSnapshotArchive struct {
files []*zip.File
archive weak.Pointer[zip.Reader]
retained *zip.Reader
retained *renderCache[int, *zip.Reader]
}
// editorSnapshotBuffer 分块保存压缩数据,写入完成后支持并发随机读取
@@ -121,7 +121,7 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
return nil, err
}
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 {
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
}
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 {
return archive, nil
}
@@ -183,8 +188,14 @@ func (e *Editor) copySnapshotArchive(ctx context.Context, source *zip.Reader) (*
return nil, err
}
cached := &editorSnapshotArchive{files: slices.Clone(source.File), archive: weak.Make(reader)}
if len(data.chunks) <= editorSnapshotArchiveLimit/editorSnapshotChunkSize {
cached.retained = reader
limit := int64(editorSnapshotArchiveLimit)
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
return reader, nil
@@ -530,7 +541,7 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
if e.source != nil {
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 {
r.files[name] = data
return nil
@@ -562,7 +573,7 @@ func (e *Editor) resourceReader() (*Reader, error) {
if err := e.validate(); err != nil {
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 {
r.files[name] = data
return nil