mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
eb20f66ffb
commit
075797402d
41 files changed
+633
-125
No files matched your search
+10
-3
@@ -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,7 +1451,8 @@ func (e *businessEngine) convertFile(args []platformValue) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
options := ofdgo.ConvertOptions{
|
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,
|
Pool: e.conversionPool,
|
||||||
SkipUnchanged: true,
|
SkipUnchanged: true,
|
||||||
Backends: &backends,
|
Backends: &backends,
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}{}:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
+3
-1
@@ -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,7 +256,8 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
|
|||||||
return Box{}, err
|
return Box{}, err
|
||||||
}
|
}
|
||||||
local := pdfImporter{
|
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,
|
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),
|
||||||
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
|
resolveFile: p.resolveFile, resolveReference: p.resolveReference,
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
+245
-24
@@ -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
|
||||||
@@ -43,19 +79,178 @@ type renderCacheLoad[V any] struct {
|
|||||||
|
|
||||||
// renderCacheEntry 保存缓存值及内存成本
|
// renderCacheEntry 保存缓存值及内存成本
|
||||||
type renderCacheEntry[K comparable, V any] struct {
|
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 {
|
||||||
|
|||||||
@@ -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 转换光栅曲线,仅在输出边界展开椭圆弧
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ type stampContentKey struct {
|
|||||||
|
|
||||||
// stampContentState 保存有界的共享签章编译结果及配置
|
// stampContentState 保存有界的共享签章编译结果及配置
|
||||||
type stampContentState struct {
|
type stampContentState struct {
|
||||||
config RenderBackends
|
resources *Resources
|
||||||
cache renderCache[stampContentKey, []RasterCommand]
|
config RenderBackends
|
||||||
|
cache renderCache[stampContentKey, []RasterCommand]
|
||||||
}
|
}
|
||||||
|
|
||||||
// stampContentStateKey 隔离渲染器私有的共享签章数据
|
// stampContentStateKey 隔离渲染器私有的共享签章数据
|
||||||
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 排除与裁剪边界分离的完整轮廓,保留相交轮廓的曲线和绕向
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user