From 7b9b96119685fbd6e9293467a47eaed568fd8d52 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 30 Sep 2026 11:44:15 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E6=80=A7=E8=83=BD=E6=8F=90=E5=8D=87):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BC=96=E8=BE=91=E6=80=A7=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 165 ++---------------------- ofdgo_editor_batch.go | 97 ++++++++------- ofdgo_editor_selection.go | 179 +++++++++++++++++++++++++++ ofdgo_font_backend.go | 17 ++- ofdgo_renderer.go | 4 + ofdgo_renderer_font_loader_canvas.go | 16 ++- ofdgo_renderer_state_canvas.go | 19 +++ 9 files changed, 300 insertions(+), 203 deletions(-) create mode 100644 ofdgo_editor_selection.go diff --git a/go.mod b/go.mod index 2f6dd63..3f76e8f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20260929170129-585f0de1a090 + github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8 golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 7c5f197..6772ebe 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260929170129-585f0de1a090 h1:G3uFJQPEm+eHQEdKIlyrabcK/TnF7N3xNiOKe/Ib1EQ= -github.com/xiaoqidun/pdfgo v0.0.0-20260929170129-585f0de1a090/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8 h1:19c9Wr9PPJ/GEh4AlrDqiUgucq1bYSIvughgsPKvNis= +github.com/xiaoqidun/pdfgo v0.0.0-20260930033808-2918854e21c8/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index a193346..ea5d49a 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -1725,169 +1725,22 @@ func changeSelection(args []js.Value) (any, error) { // 入参: page 页面, ids 标识, operation 操作, direction 方向 // 返回: error 错误信息 func arrangeEditorSelection(page int, ids []string, operation, direction string) error { - if operation == "align" && !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, direction) || operation == "distribute" && direction != "horizontal" && direction != "vertical" { - return fmt.Errorf("invalid arrangement direction") + objects, annotations := splitEditorSelection(ids) + switch operation { + case "align": + return currentEditor.AlignSelection(page, objects, annotations, direction) + case "distribute": + return currentEditor.DistributeSelection(page, objects, annotations, direction) + default: + return fmt.Errorf("invalid arrangement operation %q", operation) } - boxes := make([]ofdgo.Box, len(ids)) - var target ofdgo.Box - for i, id := range ids { - objects, annotations := splitEditorSelection([]string{id}) - box, err := editorSelectionBounds(page, objects, annotations) - if err != nil { - return err - } - boxes[i] = box - if i == 0 { - target = box - } else { - x, y := math.Min(target.X, box.X), math.Min(target.Y, box.Y) - target = ofdgo.Box{X: x, Y: y, W: math.Max(target.X+target.W, box.X+box.W) - x, H: math.Max(target.Y+target.H, box.Y+box.H) - y} - } - } - if len(ids) == 1 { - content, err := currentSession.pageContent(page) - if err != nil { - return err - } - target, err = currentSession.pageBox(page, content) - if err != nil { - return err - } - } - translations := make([]ofdgo.Point, len(ids)) - if operation == "align" { - for i, box := range boxes { - switch direction { - case "left": - translations[i].X = target.X - box.X - case "center": - translations[i].X = target.X + (target.W-box.W)/2 - box.X - case "right": - translations[i].X = target.X + target.W - box.X - box.W - case "top": - translations[i].Y = target.Y - box.Y - case "middle": - translations[i].Y = target.Y + (target.H-box.H)/2 - box.Y - case "bottom": - translations[i].Y = target.Y + target.H - box.Y - box.H - } - } - } else { - if len(ids) < 3 { - return nil - } - order := make([]int, len(ids)) - position := func(i int) float64 { - if direction == "vertical" { - return boxes[i].Y - } - return boxes[i].X - } - size := func(i int) float64 { - if direction == "vertical" { - return boxes[i].H - } - return boxes[i].W - } - for i := range order { - order[i] = i - } - slices.SortStableFunc(order, func(a, b int) int { - if position(a) < position(b) { - return -1 - } - if position(a) > position(b) { - return 1 - } - return 0 - }) - total := 0.0 - for _, i := range order { - total += size(i) - } - first, last := order[0], order[len(order)-1] - gap := (position(last) + size(last) - position(first) - total) / float64(len(order)-1) - next := position(first) - for _, i := range order { - if direction == "vertical" { - translations[i].Y = next - position(i) - } else { - translations[i].X = next - position(i) - } - next += size(i) + gap - } - } - for i, id := range ids { - objects, annotations := splitEditorSelection([]string{id}) - matrix := ofdgo.TranslationMatrix(translations[i].X, translations[i].Y) - if err := currentEditor.TransformObjectsMatrix(page, objects, matrix); err != nil { - return err - } - if err := currentEditor.TransformAnnotations(page, annotations, matrix); err != nil { - return err - } - } - return nil } // editorSelectionBounds 计算混合选区的实际内容范围 // 入参: page 页面索引, objects 正文标识, annotations 注解标识 // 返回: ofdgo.Box 页面毫米范围, error 错误信息 func editorSelectionBounds(page int, objects, annotations []string) (ofdgo.Box, error) { - content, err := currentEditor.Page(page) - if err != nil { - return ofdgo.Box{}, err - } - var bounds ofdgo.Box - add := func(box ofdgo.Box) { - if box.W <= 0 || box.H <= 0 { - return - } - if bounds.W <= 0 || bounds.H <= 0 { - bounds = box - return - } - x, y := math.Min(bounds.X, box.X), math.Min(bounds.Y, box.Y) - bounds = ofdgo.Box{X: x, Y: y, W: math.Max(bounds.X+bounds.W, box.X+box.W) - x, H: math.Max(bounds.Y+bounds.H, box.Y+box.H) - y} - } - for _, layer := range content.Content.Layer { - for _, object := range layer.Objects { - var id string - switch object.Type { - case "TextObject": - id = object.TextObject.ID - case "PathObject": - id = object.PathObject.ID - case "ImageObject": - id = object.ImageObject.ID - case "CompositeObject", "CompositeGraphicUnit": - id = object.CompositeGraphicUnit.ID - } - if slices.Contains(objects, id) { - box, err := currentSession.Renderer.ObjectBounds(object, layer.DrawParam) - if err != nil { - return bounds, err - } - add(box) - } - } - } - for _, annotation := range currentSession.Reader.Annots[content.ID] { - if slices.Contains(annotations, annotation.ID) { - box, _, err := currentSession.Renderer.AnnotationGeometry(annotation) - if err != nil { - return bounds, err - } - if box.W <= 0 || box.H <= 0 { - box, _ = ofdgo.ParseBox(annotation.Appearance.Boundary) - } - add(box) - } - } - if bounds.W <= 0 || bounds.H <= 0 { - return bounds, fmt.Errorf("selection has no bounds") - } - return bounds, nil + return currentEditor.SelectionBounds(page, objects, annotations) } // captureEditorSelection 捕获正文与注解的独立副本 diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index 264c951..83d4e99 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -52,12 +52,10 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) { } slices.SortFunc(indexes, compareEditorPosition) objects := make([]GraphicObject, len(indexes)) + orderable := make(map[*editorXML]bool) for i, index := range indexes { object := e.pages[page].Content.Layer[index.layer].Objects[index.index] - capability, err := e.ObjectCapabilities(page, editorObjectID(object)) - if err != nil { - return nil, err - } + capability := e.objectCapabilities(object, orderable) if !capability.Copy { return nil, fmt.Errorf("object %q cannot be copied: %w", editorObjectID(object), capability.editError()) } @@ -208,17 +206,15 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error { if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) { return fmt.Errorf("invalid object order %q", order) } - _, indexes, err := e.selectedObjects(page, ids) + selectedObjects, indexes, err := e.selectedObjects(page, ids) if err != nil || len(indexes) == 0 { return err } layer := &e.pages[page].Content.Layer[indexes[0].layer] _, siblings := e.objectOrderIndexes(page, layer, ids[0]) - for i, id := range ids { - capability, err := e.ObjectCapabilities(page, id) - if err != nil { - return err - } + orderable := make(map[*editorXML]bool) + for i, object := range selectedObjects { + capability := e.objectCapabilities(object, orderable) if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) { return fmt.Errorf("ordering requires editable objects in the same container") } @@ -349,17 +345,15 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error { // 入参: page 页面索引, ids 对象标识 // 返回: error 错误信息 func (e *Editor) DeleteObjects(page int, ids []string) error { - _, indexes, err := e.selectedObjects(page, ids) + objects, indexes, err := e.selectedObjects(page, ids) if err != nil || len(indexes) == 0 { return err } - for _, id := range ids { - capability, err := e.ObjectCapabilities(page, id) - if err != nil { - return err - } + orderable := make(map[*editorXML]bool) + for _, object := range objects { + capability := e.objectCapabilities(object, orderable) if !capability.Delete { - return fmt.Errorf("object %q cannot be deleted: %w", id, capability.editError()) + return fmt.Errorf("object %q cannot be deleted: %w", editorObjectID(object), capability.editError()) } } layers := copyEditorPage(e.pages[page]).Content.Layer @@ -549,15 +543,13 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve } after := make([]GraphicObject, len(objects)) var beforeOrigins, afterOrigins map[string]*editorObjectOrigin + orderable := make(map[*editorXML]bool) for i, object := range objects { object.origin = nil preserved := preserveContent origin := e.objectOrigin(ids[i]) if origin != nil { - capability, err := e.ObjectCapabilities(page, ids[i]) - if err != nil { - return err - } + capability := e.objectCapabilities(before[i], orderable) if !capability.Transform { return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError()) } @@ -664,28 +656,39 @@ func editorAlignment(box, target Box, alignment string) Matrix { // 入参: page 页面索引, ids 对象标识,不得重复 // 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息 func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []editorObjectPosition, error) { - if _, err := e.page(page); err != nil { + content, err := e.page(page) + if err != nil { return nil, nil, err } objects := make([]GraphicObject, len(ids)) indexes := make([]editorObjectPosition, len(ids)) - seen := make(map[string]bool, len(ids)) + if len(ids) == 0 { + return objects, indexes, nil + } + selected := make(map[string]int, len(ids)) for i, id := range ids { - if seen[id] { + if _, exists := selected[id]; exists { return nil, nil, fmt.Errorf("duplicate object %q", id) } - seen[id] = true - layer, index, err := e.findObject(page, id) - if err != nil { - return nil, nil, err - } - for j := range e.pages[page].Content.Layer { - if &e.pages[page].Content.Layer[j] == layer { - indexes[i] = editorObjectPosition{j, index} - break + selected[id] = i + } + for i, layer := range content.Content.Layer { + for j, object := range layer.Objects { + id := editorObjectID(object) + if index, ok := selected[id]; ok && id != "" { + objects[index] = object + indexes[index] = editorObjectPosition{i, j} + delete(selected, id) + if len(selected) == 0 { + return objects, indexes, nil + } } } - objects[i] = layer.Objects[index] + } + for _, id := range ids { + if _, missing := selected[id]; missing { + return nil, nil, fmt.Errorf("object %q not found on page %d", id, page) + } } return objects, indexes, nil } @@ -711,11 +714,9 @@ func editorObjectID(object GraphicObject) string { // 入参: page 页面索引, objects 待度量的对象 // 返回: []Box 对象范围, error 错误信息 func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) { + orderable := make(map[*editorXML]bool) for _, object := range objects { - capability, err := e.ObjectCapabilities(page, editorObjectID(object)) - if err != nil { - return nil, err - } + capability := e.objectCapabilities(object, orderable) if !capability.Arrange { return nil, fmt.Errorf("object %q cannot be arranged: %w", editorObjectID(object), capability.editError()) } @@ -729,13 +730,25 @@ func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) return nil, err } renderer := e.newRenderer(reader) + objectLayers := make(map[string]int, len(objects)) + for _, object := range objects { + objectLayers[editorObjectID(object)] = -1 + } + for i, layer := range e.pages[page].Content.Layer { + for _, object := range layer.Objects { + id := editorObjectID(object) + if index, wanted := objectLayers[id]; wanted && index < 0 { + objectLayers[id] = i + } + } + } boxes := make([]Box, len(objects)) for i, object := range objects { - layer, _, findErr := e.findObject(page, editorObjectID(object)) - if findErr != nil { - return nil, findErr + layer := objectLayers[editorObjectID(object)] + if layer < 0 { + return nil, fmt.Errorf("object %q not found on page %d", editorObjectID(object), page) } - boxes[i], err = renderer.ObjectBounds(object, layer.DrawParam) + boxes[i], err = renderer.ObjectBounds(object, e.pages[page].Content.Layer[layer].DrawParam) if err != nil { return nil, err } diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go new file mode 100644 index 0000000..689d58b --- /dev/null +++ b/ofdgo_editor_selection.go @@ -0,0 +1,179 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "fmt" + "slices" +) + +// SelectionBounds 获取正文与注解混合选区的实际范围,复用同一次度量的资源缓存 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识,各列表不得重复 +// 返回: Box 页面毫米范围, error 标识或度量错误 +func (e *Editor) SelectionBounds(page int, objects, annotations []string) (Box, error) { + boxes, _, _, err := e.selectionGeometry(page, objects, annotations) + if err != nil { + return Box{}, err + } + var bounds Box + for _, box := range boxes { + bounds = unionTextBox(bounds, box) + } + if bounds.W <= 0 || bounds.H <= 0 { + return Box{}, fmt.Errorf("selection has no visible bounds") + } + return bounds, nil +} + +// AlignSelection 将正文与注解统一对齐,单选对齐页面,多选对齐选区,一次撤销恢复全部 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, alignment 为left、center、right、top、middle或bottom +// 返回: error 标识、度量或编辑错误 +func (e *Editor) AlignSelection(page int, objects, annotations []string, alignment string) error { + if !slices.Contains([]string{"left", "center", "right", "top", "middle", "bottom"}, alignment) { + return fmt.Errorf("invalid alignment %q", alignment) + } + boxes, _, target, err := e.selectionGeometry(page, objects, annotations) + if err != nil || len(boxes) == 0 { + return err + } + if len(boxes) > 1 { + target = Box{} + for _, box := range boxes { + target = unionTextBox(target, box) + } + } + matrices := make([]Matrix, len(boxes)) + for i, box := range boxes { + matrices[i] = editorAlignment(box, target, alignment) + } + return e.transformSelection(page, objects, annotations, matrices) +} + +// DistributeSelection 按正文与注解实际范围等距分布,固定两端并校验位置顺序,一次撤销恢复全部 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, axis 为horizontal或vertical +// 返回: error 标识、度量、重叠或编辑错误 +func (e *Editor) DistributeSelection(page int, objects, annotations []string, axis string) error { + if axis != "horizontal" && axis != "vertical" { + return fmt.Errorf("invalid distribution axis %q", axis) + } + boxes, positions, _, err := e.selectionGeometry(page, objects, annotations) + if err != nil || len(boxes) < 3 { + return err + } + matrices, err := editorDistribution(boxes, positions, axis) + if err != nil { + return err + } + return e.transformSelection(page, objects, annotations, matrices) +} + +// selectionGeometry 一次读取页面与资源,按正文、注解标识顺序度量并保留绘制位置 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识 +// 返回: []Box 成员范围, []editorObjectPosition 绘制位置, Box 页面范围, error 标识或度量错误 +func (e *Editor) selectionGeometry(page int, objects, annotations []string) ([]Box, []editorObjectPosition, Box, error) { + items, positions, err := e.selectedObjects(page, objects) + if err != nil { + return nil, nil, Box{}, err + } + reader, err := e.Reader() + if err != nil { + return nil, nil, Box{}, err + } + defer reader.Close() + content, err := reader.PageContentByIndex(page) + if err != nil { + return nil, nil, Box{}, err + } + renderer := e.newRenderer(reader) + paper, err := renderer.GetPageBox(content) + if err != nil { + return nil, nil, Box{}, err + } + boxes := make([]Box, 0, len(items)+len(annotations)) + for i, item := range items { + box, err := renderer.ObjectBounds(item, e.pages[page].Content.Layer[positions[i].layer].DrawParam) + if err != nil { + return nil, nil, Box{}, err + } + boxes = append(boxes, box) + } + list := reader.Annots[content.ID] + lookup := make(map[string]int, len(list)) + for i, annotation := range list { + lookup[annotation.ID] = i + } + seen := make(map[string]bool, len(annotations)) + for _, id := range annotations { + index, found := lookup[id] + if seen[id] || !found { + return nil, nil, Box{}, fmt.Errorf("duplicate or missing annotation %q", id) + } + seen[id] = true + box, _, err := renderer.AnnotationGeometry(list[index]) + if err != nil { + return nil, nil, Box{}, err + } + if box.W <= 0 || box.H <= 0 { + box, err = ParseBox(list[index].Appearance.Boundary) + if err != nil { + return nil, nil, Box{}, err + } + } + boxes = append(boxes, box) + positions = append(positions, editorObjectPosition{layer: len(content.Content.Layer), index: index}) + } + for _, box := range boxes { + if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || box.W <= 0 || box.H <= 0 { + return nil, nil, Box{}, fmt.Errorf("selection member has no visible bounds") + } + } + return boxes, positions, paper, nil +} + +// transformSelection 原子应用成员平移,跳过未改变的成员及历史记录 +// 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrices 按正文、注解排列的平移 +// 返回: error 编辑错误 +func (e *Editor) transformSelection(page int, objects, annotations []string, matrices []Matrix) error { + return e.Transaction(func(edit *Editor) error { + items, _, err := edit.selectedObjects(page, objects) + if err != nil { + return err + } + changes := make(map[string]Matrix, len(objects)) + var updates []GraphicObject + for i, id := range objects { + if matrices[i] != IdentityMatrix { + changes[id] = matrices[i] + updates = append(updates, items[i]) + } + } + if len(updates) != 0 { + if err := edit.transformObjects(page, updates, func(object GraphicObject) (GraphicObject, error) { + return edit.transformMatrix(object, changes[editorObjectID(object)]) + }); err != nil { + return err + } + } + for i, id := range annotations { + matrix := matrices[len(objects)+i] + if matrix != IdentityMatrix { + if err := edit.MoveAnnotations(page, []string{id}, matrix.e, matrix.f); err != nil { + return err + } + } + } + return nil + }) +} diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index d612217..8ece160 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -14,7 +14,10 @@ package ofdgo -import "fmt" +import ( + "crypto/sha256" + "fmt" +) // FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位 // VerticalMetrics依次返回非负的上升高度、下降高度和行间隙 @@ -141,6 +144,7 @@ func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) { } // PrepareFont 统一处理内嵌字体包装和索引映射,供各编译器复用 +// 子渲染器按原始内容复用只读包装结果,各文档保留独立的来源信息 // 入参: id 字体资源标识 // 返回: *PreparedFont 绘制字体, error 字体解析错误 func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { @@ -154,6 +158,12 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { prepared := &PreparedFont{ResolvedFont: resolved} definition := r.Reader.fontCache[id] if definition != nil && definition.FontFile != "" { + key := sha256.Sum256(resolved.Data) + if content, ok := r.fontPreparations.get(key); ok { + prepared.Data, prepared.Glyphs, prepared.CIDs, prepared.digest = content.Data, content.Glyphs, content.CIDs, content.digest + r.preparedFonts[id] = prepared + return prepared, nil + } prepared.CIDs = getCFFCIDRuneMap(resolved.Data) if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil { prepared.Data = data @@ -171,6 +181,9 @@ func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { } } } + prepared.digest = sha256.Sum256(prepared.Data) + cost := min(len(prepared.Data), r.fontPreparations.limit)*2 + (len(prepared.Glyphs)+len(prepared.CIDs))*48 + 256 + r.fontPreparations.put(key, PreparedFont{ResolvedFont: ResolvedFont{Data: prepared.Data}, Glyphs: prepared.Glyphs, CIDs: prepared.CIDs, digest: prepared.digest}, cost) } r.preparedFonts[id] = prepared return prepared, nil @@ -184,7 +197,9 @@ func (r *Renderer) resetFontCache() { // resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存 func (r *Renderer) resetFontBackendCache() { + r.sharedBackendStates = make(map[any]any) r.preparedFonts = make(map[string]*PreparedFont) + r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 16 << 20} r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult) if r.fontSourcesCache != nil { r.fontSourcesCache.fallback = ResolvedFont{} diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index ca7d4bd..76c04f3 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -46,8 +46,10 @@ type Renderer struct { textOnly bool backends RenderBackends backendStates map[any]any + sharedBackendStates map[any]any resolvedFonts map[resolvedFontKey]resolvedFontResult preparedFonts map[string]*PreparedFont + fontPreparations *renderCache[[32]byte, PreparedFont] fontSourcesCache *fontSourceCache fontMetrics renderCache[[32]byte, FontMetrics] glyphOutlines renderCache[glyphOutlineKey, GeometryPath] @@ -96,6 +98,8 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer { opts = append(opts, WithFontFS(r.fontFS...)) } renderer := NewRenderer(reader, opts...) + renderer.sharedBackendStates = r.sharedBackendStates + renderer.fontPreparations = r.fontPreparations renderer.decodeImages = r.decodeImages renderer.TransparentBackground = r.TransparentBackground return renderer diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go index c46d6e6..576d1b5 100644 --- a/ofdgo_renderer_font_loader_canvas.go +++ b/ofdgo_renderer_font_loader_canvas.go @@ -14,7 +14,11 @@ package ofdgo -import "github.com/tdewolff/canvas" +import ( + "crypto/sha256" + + "github.com/tdewolff/canvas" +) // loadFont 加载字体 // 入参: fontID 字体ID @@ -37,12 +41,22 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { } fontData := resolved.Data fontStyle := canvasFontStyle(of) + if resolved.digest == ([32]byte{}) { + resolved.digest = sha256.Sum256(fontData) + } + key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle} + cache := r.canvasFontFamilies() + if ff, ok := cache.get(key); ok { + r.canvasState().fontMap[fontID] = ff + return ff + } ff := canvas.NewFontFamily(of.FontName) if err := ff.LoadFont(fontData, 0, fontStyle); err != nil { r.renderError = err return nil } r.canvasState().fontMap[fontID] = ff + cache.put(key, ff, min(len(fontData), cache.limit)*8+256) return ff } diff --git a/ofdgo_renderer_state_canvas.go b/ofdgo_renderer_state_canvas.go index 2014881..42eb685 100644 --- a/ofdgo_renderer_state_canvas.go +++ b/ofdgo_renderer_state_canvas.go @@ -20,6 +20,13 @@ import ( "github.com/tdewolff/canvas" ) +// canvasFontKey 按字体内容、名称及样式区分可复用的字体族,不依赖文档资源编号 +type canvasFontKey struct { + digest [32]byte + name string + style canvas.FontStyle +} + // canvasBackendState 保存Canvas字体、字形和编码图片适配缓存 type canvasBackendState struct { images renderCache[*EncodedImage, image.Image] @@ -29,6 +36,18 @@ type canvasBackendState struct { fontOutlines map[*canvas.Font]*sfntOutliner } +// canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量 +// 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存 +func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] { + key := CanvasBackend{} + if state, ok := r.sharedBackendStates[key]; ok { + return state.(*renderCache[canvasFontKey, *canvas.FontFamily]) + } + cache := &renderCache[canvasFontKey, *canvas.FontFamily]{limit: 32 << 20} + r.sharedBackendStates[key] = cache + return cache +} + // canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露 // 返回: *canvasBackendState Canvas缓存 func (r *Renderer) canvasState() *canvasBackendState {