mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +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/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
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/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.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/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 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
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-20260930033808-2918854e21c8 h1:19c9Wr9PPJ/GEh4AlrDqiUgucq1bYSIvughgsPKvNis=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260929170129-585f0de1a090/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
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 方向
|
// 入参: page 页面, ids 标识, operation 操作, direction 方向
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func arrangeEditorSelection(page int, ids []string, operation, direction string) 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" {
|
objects, annotations := splitEditorSelection(ids)
|
||||||
return fmt.Errorf("invalid arrangement direction")
|
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 计算混合选区的实际内容范围
|
// editorSelectionBounds 计算混合选区的实际内容范围
|
||||||
// 入参: page 页面索引, objects 正文标识, annotations 注解标识
|
// 入参: page 页面索引, objects 正文标识, annotations 注解标识
|
||||||
// 返回: ofdgo.Box 页面毫米范围, error 错误信息
|
// 返回: ofdgo.Box 页面毫米范围, error 错误信息
|
||||||
func editorSelectionBounds(page int, objects, annotations []string) (ofdgo.Box, error) {
|
func editorSelectionBounds(page int, objects, annotations []string) (ofdgo.Box, error) {
|
||||||
content, err := currentEditor.Page(page)
|
return currentEditor.SelectionBounds(page, objects, annotations)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// captureEditorSelection 捕获正文与注解的独立副本
|
// captureEditorSelection 捕获正文与注解的独立副本
|
||||||
|
|||||||
+55
-42
@@ -52,12 +52,10 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
|||||||
}
|
}
|
||||||
slices.SortFunc(indexes, compareEditorPosition)
|
slices.SortFunc(indexes, compareEditorPosition)
|
||||||
objects := make([]GraphicObject, len(indexes))
|
objects := make([]GraphicObject, len(indexes))
|
||||||
|
orderable := make(map[*editorXML]bool)
|
||||||
for i, index := range indexes {
|
for i, index := range indexes {
|
||||||
object := e.pages[page].Content.Layer[index.layer].Objects[index.index]
|
object := e.pages[page].Content.Layer[index.layer].Objects[index.index]
|
||||||
capability, err := e.ObjectCapabilities(page, editorObjectID(object))
|
capability := e.objectCapabilities(object, orderable)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !capability.Copy {
|
if !capability.Copy {
|
||||||
return nil, fmt.Errorf("object %q cannot be copied: %w", editorObjectID(object), capability.editError())
|
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) {
|
if !slices.Contains([]string{"up", "down", "top", "bottom"}, order) {
|
||||||
return fmt.Errorf("invalid object order %q", 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 {
|
if err != nil || len(indexes) == 0 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
layer := &e.pages[page].Content.Layer[indexes[0].layer]
|
layer := &e.pages[page].Content.Layer[indexes[0].layer]
|
||||||
_, siblings := e.objectOrderIndexes(page, layer, ids[0])
|
_, siblings := e.objectOrderIndexes(page, layer, ids[0])
|
||||||
for i, id := range ids {
|
orderable := make(map[*editorXML]bool)
|
||||||
capability, err := e.ObjectCapabilities(page, id)
|
for i, object := range selectedObjects {
|
||||||
if err != nil {
|
capability := e.objectCapabilities(object, orderable)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !capability.Order || indexes[i].layer != indexes[0].layer || !slices.Contains(siblings, indexes[i].index) {
|
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")
|
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 对象标识
|
// 入参: page 页面索引, ids 对象标识
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) DeleteObjects(page int, ids []string) 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 {
|
if err != nil || len(indexes) == 0 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, id := range ids {
|
orderable := make(map[*editorXML]bool)
|
||||||
capability, err := e.ObjectCapabilities(page, id)
|
for _, object := range objects {
|
||||||
if err != nil {
|
capability := e.objectCapabilities(object, orderable)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !capability.Delete {
|
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
|
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))
|
after := make([]GraphicObject, len(objects))
|
||||||
var beforeOrigins, afterOrigins map[string]*editorObjectOrigin
|
var beforeOrigins, afterOrigins map[string]*editorObjectOrigin
|
||||||
|
orderable := make(map[*editorXML]bool)
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
object.origin = nil
|
object.origin = nil
|
||||||
preserved := preserveContent
|
preserved := preserveContent
|
||||||
origin := e.objectOrigin(ids[i])
|
origin := e.objectOrigin(ids[i])
|
||||||
if origin != nil {
|
if origin != nil {
|
||||||
capability, err := e.ObjectCapabilities(page, ids[i])
|
capability := e.objectCapabilities(before[i], orderable)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !capability.Transform {
|
if !capability.Transform {
|
||||||
return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError())
|
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 对象标识,不得重复
|
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||||
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
||||||
func (e *Editor) selectedObjects(page int, ids []string) ([]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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
objects := make([]GraphicObject, len(ids))
|
objects := make([]GraphicObject, len(ids))
|
||||||
indexes := make([]editorObjectPosition, 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 {
|
for i, id := range ids {
|
||||||
if seen[id] {
|
if _, exists := selected[id]; exists {
|
||||||
return nil, nil, fmt.Errorf("duplicate object %q", id)
|
return nil, nil, fmt.Errorf("duplicate object %q", id)
|
||||||
}
|
}
|
||||||
seen[id] = true
|
selected[id] = i
|
||||||
layer, index, err := e.findObject(page, id)
|
}
|
||||||
if err != nil {
|
for i, layer := range content.Content.Layer {
|
||||||
return nil, nil, err
|
for j, object := range layer.Objects {
|
||||||
}
|
id := editorObjectID(object)
|
||||||
for j := range e.pages[page].Content.Layer {
|
if index, ok := selected[id]; ok && id != "" {
|
||||||
if &e.pages[page].Content.Layer[j] == layer {
|
objects[index] = object
|
||||||
indexes[i] = editorObjectPosition{j, index}
|
indexes[index] = editorObjectPosition{i, j}
|
||||||
break
|
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
|
return objects, indexes, nil
|
||||||
}
|
}
|
||||||
@@ -711,11 +714,9 @@ func editorObjectID(object GraphicObject) string {
|
|||||||
// 入参: page 页面索引, objects 待度量的对象
|
// 入参: page 页面索引, objects 待度量的对象
|
||||||
// 返回: []Box 对象范围, error 错误信息
|
// 返回: []Box 对象范围, error 错误信息
|
||||||
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
|
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
|
||||||
|
orderable := make(map[*editorXML]bool)
|
||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
capability, err := e.ObjectCapabilities(page, editorObjectID(object))
|
capability := e.objectCapabilities(object, orderable)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !capability.Arrange {
|
if !capability.Arrange {
|
||||||
return nil, fmt.Errorf("object %q cannot be arranged: %w", editorObjectID(object), capability.editError())
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
renderer := e.newRenderer(reader)
|
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))
|
boxes := make([]Box, len(objects))
|
||||||
for i, object := range objects {
|
for i, object := range objects {
|
||||||
layer, _, findErr := e.findObject(page, editorObjectID(object))
|
layer := objectLayers[editorObjectID(object)]
|
||||||
if findErr != nil {
|
if layer < 0 {
|
||||||
return nil, findErr
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
package ofdgo
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位
|
// FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位
|
||||||
// VerticalMetrics依次返回非负的上升高度、下降高度和行间隙
|
// VerticalMetrics依次返回非负的上升高度、下降高度和行间隙
|
||||||
@@ -141,6 +144,7 @@ func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PrepareFont 统一处理内嵌字体包装和索引映射,供各编译器复用
|
// PrepareFont 统一处理内嵌字体包装和索引映射,供各编译器复用
|
||||||
|
// 子渲染器按原始内容复用只读包装结果,各文档保留独立的来源信息
|
||||||
// 入参: id 字体资源标识
|
// 入参: id 字体资源标识
|
||||||
// 返回: *PreparedFont 绘制字体, error 字体解析错误
|
// 返回: *PreparedFont 绘制字体, error 字体解析错误
|
||||||
func (r *Renderer) PrepareFont(id string) (*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}
|
prepared := &PreparedFont{ResolvedFont: resolved}
|
||||||
definition := r.Reader.fontCache[id]
|
definition := r.Reader.fontCache[id]
|
||||||
if definition != nil && definition.FontFile != "" {
|
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)
|
prepared.CIDs = getCFFCIDRuneMap(resolved.Data)
|
||||||
if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil {
|
if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil {
|
||||||
prepared.Data = data
|
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
|
r.preparedFonts[id] = prepared
|
||||||
return prepared, nil
|
return prepared, nil
|
||||||
@@ -184,7 +197,9 @@ func (r *Renderer) resetFontCache() {
|
|||||||
|
|
||||||
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
|
// resetFontBackendCache 清理后端字体对象,保留字体文件和图片缓存
|
||||||
func (r *Renderer) resetFontBackendCache() {
|
func (r *Renderer) resetFontBackendCache() {
|
||||||
|
r.sharedBackendStates = make(map[any]any)
|
||||||
r.preparedFonts = make(map[string]*PreparedFont)
|
r.preparedFonts = make(map[string]*PreparedFont)
|
||||||
|
r.fontPreparations = &renderCache[[32]byte, PreparedFont]{limit: 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{}
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ type Renderer struct {
|
|||||||
textOnly bool
|
textOnly bool
|
||||||
backends RenderBackends
|
backends RenderBackends
|
||||||
backendStates map[any]any
|
backendStates map[any]any
|
||||||
|
sharedBackendStates map[any]any
|
||||||
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
||||||
preparedFonts map[string]*PreparedFont
|
preparedFonts map[string]*PreparedFont
|
||||||
|
fontPreparations *renderCache[[32]byte, PreparedFont]
|
||||||
fontSourcesCache *fontSourceCache
|
fontSourcesCache *fontSourceCache
|
||||||
fontMetrics renderCache[[32]byte, FontMetrics]
|
fontMetrics renderCache[[32]byte, FontMetrics]
|
||||||
glyphOutlines renderCache[glyphOutlineKey, GeometryPath]
|
glyphOutlines renderCache[glyphOutlineKey, GeometryPath]
|
||||||
@@ -96,6 +98,8 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
|||||||
opts = append(opts, WithFontFS(r.fontFS...))
|
opts = append(opts, WithFontFS(r.fontFS...))
|
||||||
}
|
}
|
||||||
renderer := NewRenderer(reader, opts...)
|
renderer := NewRenderer(reader, opts...)
|
||||||
|
renderer.sharedBackendStates = r.sharedBackendStates
|
||||||
|
renderer.fontPreparations = r.fontPreparations
|
||||||
renderer.decodeImages = r.decodeImages
|
renderer.decodeImages = r.decodeImages
|
||||||
renderer.TransparentBackground = r.TransparentBackground
|
renderer.TransparentBackground = r.TransparentBackground
|
||||||
return renderer
|
return renderer
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import "github.com/tdewolff/canvas"
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
|
)
|
||||||
|
|
||||||
// loadFont 加载字体
|
// loadFont 加载字体
|
||||||
// 入参: fontID 字体ID
|
// 入参: fontID 字体ID
|
||||||
@@ -37,12 +41,22 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
}
|
}
|
||||||
fontData := resolved.Data
|
fontData := resolved.Data
|
||||||
fontStyle := canvasFontStyle(of)
|
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)
|
ff := canvas.NewFontFamily(of.FontName)
|
||||||
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil {
|
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
r.canvasState().fontMap[fontID] = ff
|
r.canvasState().fontMap[fontID] = ff
|
||||||
|
cache.put(key, ff, min(len(fontData), cache.limit)*8+256)
|
||||||
return ff
|
return ff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// canvasFontKey 按字体内容、名称及样式区分可复用的字体族,不依赖文档资源编号
|
||||||
|
type canvasFontKey struct {
|
||||||
|
digest [32]byte
|
||||||
|
name string
|
||||||
|
style canvas.FontStyle
|
||||||
|
}
|
||||||
|
|
||||||
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
|
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
|
||||||
type canvasBackendState struct {
|
type canvasBackendState struct {
|
||||||
images renderCache[*EncodedImage, image.Image]
|
images renderCache[*EncodedImage, image.Image]
|
||||||
@@ -29,6 +36,18 @@ type canvasBackendState struct {
|
|||||||
fontOutlines map[*canvas.Font]*sfntOutliner
|
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私有缓存,按需创建且不向其他后端暴露
|
// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露
|
||||||
// 返回: *canvasBackendState Canvas缓存
|
// 返回: *canvasBackendState Canvas缓存
|
||||||
func (r *Renderer) canvasState() *canvasBackendState {
|
func (r *Renderer) canvasState() *canvasBackendState {
|
||||||
|
|||||||
Reference in new issue
Block a user