mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
e891600510
commit
7b9b961196
9 files changed
+300
-203
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+9
-156
@@ -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 捕获正文与注解的独立副本
|
||||
|
||||
+55
-42
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
+16
-1
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user