mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
feat(写入增强): 新增对象编辑与页面管理
This commit is contained in:
1 parent
b71bb6bfb8
commit
1447124d0f
2 files changed
+224
-53
No files matched your search
+194
-27
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,7 +33,7 @@ import (
|
||||
)
|
||||
|
||||
// Editor 新建OFD文档,长度单位为毫米,页面索引从0开始,实例需串行使用
|
||||
// Info可修改文档元数据,页面和资源通过方法添加,不修改已有OFD文件
|
||||
// Info可修改文档元数据,通过方法管理页面、对象和资源,不修改已有OFD文件
|
||||
type Editor struct {
|
||||
Info DocInfo
|
||||
pages []PageContent
|
||||
@@ -75,6 +76,42 @@ func NewEditor() *Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// PageCount 获取当前页数
|
||||
// 返回: int 页数
|
||||
func (e *Editor) PageCount() int {
|
||||
return len(e.pages)
|
||||
}
|
||||
|
||||
// Page 获取页面的独立副本,修改副本不影响文档
|
||||
// 入参: index 页面索引
|
||||
// 返回: *PageContent 页面内容, error 错误信息
|
||||
func (e *Editor) Page(index int) (*PageContent, error) {
|
||||
page, err := e.page(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := encodeOFDXML(func(x *ofdXML) { x.page(*page) })
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result PageContent
|
||||
if err := xml.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ID = page.ID
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// page 按索引获取内部页面
|
||||
// 入参: index 页面索引
|
||||
// 返回: *PageContent 页面内容, error 错误信息
|
||||
func (e *Editor) page(index int) (*PageContent, error) {
|
||||
if index < 0 || index >= len(e.pages) {
|
||||
return nil, fmt.Errorf("page index %d out of range", index)
|
||||
}
|
||||
return &e.pages[index], nil
|
||||
}
|
||||
|
||||
// AddPage 添加页面及默认正文图层
|
||||
// 入参: width 页面宽度, height 页面高度
|
||||
// 返回: int 页面索引, error 错误信息
|
||||
@@ -90,6 +127,52 @@ func (e *Editor) AddPage(width, height float64) (int, error) {
|
||||
return len(e.pages) - 1, nil
|
||||
}
|
||||
|
||||
// DeletePage 删除页面,不回收资源或复用标识,删除后页面索引随之变化
|
||||
// 入参: index 页面索引
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) DeletePage(index int) error {
|
||||
if _, err := e.page(index); err != nil {
|
||||
return err
|
||||
}
|
||||
e.pages = slices.Delete(e.pages, index, index+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MovePage 移动页面,保留页面及对象标识
|
||||
// 入参: from 原页面索引, to 移动后的页面索引
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) MovePage(from, to int) error {
|
||||
if _, err := e.page(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := e.page(to); err != nil {
|
||||
return err
|
||||
}
|
||||
page := e.pages[from]
|
||||
if from < to {
|
||||
copy(e.pages[from:to], e.pages[from+1:to+1])
|
||||
} else {
|
||||
copy(e.pages[to+1:from+1], e.pages[to:from])
|
||||
}
|
||||
e.pages[to] = page
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResizePage 调整页面尺寸,不缩放或移动页面内的对象
|
||||
// 入参: index 页面索引, width 页面宽度, height 页面高度
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) ResizePage(index int, width, height float64) error {
|
||||
page, err := e.page(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !finite(width) || !finite(height) || width <= 0 || height <= 0 {
|
||||
return fmt.Errorf("page dimensions must be finite and positive")
|
||||
}
|
||||
page.Area.PhysicalBox = fmt.Sprintf("0 0 %s %s", ofdNumber(width), ofdNumber(height))
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextID 分配文档内唯一标识
|
||||
// 返回: string 对象标识
|
||||
func (e *Editor) nextID() string {
|
||||
@@ -203,10 +286,89 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
||||
// 入参: page 页面索引, object 对象内容,添加后不再引用调用方的可变数据
|
||||
// 返回: string 对象标识, error 错误信息
|
||||
func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
if page < 0 || page >= len(e.pages) {
|
||||
return "", fmt.Errorf("page index %d out of range", page)
|
||||
content, err := e.page(page)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := strconv.Itoa(e.maxID + 1)
|
||||
object, err = e.prepareObject(id, object)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
e.maxID++
|
||||
content.Content.Layer[0].Objects = append(content.Content.Layer[0].Objects, object)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Object 获取对象的独立副本,修改副本不影响文档
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: GraphicObject 对象内容, error 错误信息
|
||||
func (e *Editor) Object(page int, id string) (GraphicObject, error) {
|
||||
layer, index, err := e.findObject(page, id)
|
||||
if err != nil {
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
return cloneEditorObject(layer.Objects[index])
|
||||
}
|
||||
|
||||
// UpdateObject 替换对象内容,保留原对象标识和绘制顺序,校验规则与AddObject相同
|
||||
// 入参: page 页面索引, id 对象标识, object 新内容,忽略其ID且不引用调用方的可变数据
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) UpdateObject(page int, id string, object GraphicObject) error {
|
||||
layer, index, err := e.findObject(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
object, err = e.prepareObject(id, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layer.Objects[index] = object
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteObject 删除对象,不回收资源或复用标识
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) DeleteObject(page int, id string) error {
|
||||
layer, index, err := e.findObject(page, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layer.Objects = slices.Delete(layer.Objects, index, index+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// findObject 查找正文图层中的对象
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: *Layer 所属图层, int 对象索引, error 错误信息
|
||||
func (e *Editor) findObject(page int, id string) (*Layer, int, error) {
|
||||
content, err := e.page(page)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
layer := &content.Content.Layer[0]
|
||||
for index, object := range layer.Objects {
|
||||
var objectID string
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
objectID = object.TextObject.ID
|
||||
case "PathObject":
|
||||
objectID = object.PathObject.ID
|
||||
case "ImageObject":
|
||||
objectID = object.ImageObject.ID
|
||||
}
|
||||
if objectID == id {
|
||||
return layer, index, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, fmt.Errorf("object %q not found on page %d", id, page)
|
||||
}
|
||||
|
||||
// prepareObject 校验基本对象并生成独立副本
|
||||
// 入参: id 对象标识, object 对象内容
|
||||
// 返回: GraphicObject 对象内容, error 错误信息
|
||||
func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject, error) {
|
||||
var boundary, ctm, drawParam, join string
|
||||
var alpha *int
|
||||
var clips *Clips
|
||||
@@ -216,7 +378,7 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
case "TextObject":
|
||||
obj := &object.TextObject
|
||||
if err := e.prepareText(obj); err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
obj.ID = id
|
||||
boundary, ctm, drawParam = obj.Boundary, obj.CTM, obj.DrawParam
|
||||
@@ -226,34 +388,34 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
case "PathObject":
|
||||
obj := &object.PathObject
|
||||
if err := creationPath(obj.AbbreviatedData); err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if obj.Rule != "" && obj.Rule != "NonZero" && obj.Rule != "Even-Odd" {
|
||||
return "", fmt.Errorf("invalid fill rule %q", obj.Rule)
|
||||
return GraphicObject{}, fmt.Errorf("invalid fill rule %q", obj.Rule)
|
||||
}
|
||||
if !finite(obj.LineWidth) || obj.LineWidth < 0 || !finite(obj.MiterLimit) || obj.MiterLimit < 0 {
|
||||
return "", fmt.Errorf("invalid path stroke dimensions")
|
||||
return GraphicObject{}, fmt.Errorf("invalid path stroke dimensions")
|
||||
}
|
||||
if obj.Cap != "" && obj.Cap != "Butt" && obj.Cap != "Round" && obj.Cap != "Square" {
|
||||
return "", fmt.Errorf("invalid line cap %q", obj.Cap)
|
||||
return GraphicObject{}, fmt.Errorf("invalid line cap %q", obj.Cap)
|
||||
}
|
||||
if obj.DashOffset != nil && !finite(*obj.DashOffset) {
|
||||
return "", fmt.Errorf("invalid dash offset")
|
||||
return GraphicObject{}, fmt.Errorf("invalid dash offset")
|
||||
}
|
||||
if obj.DashPattern != "" {
|
||||
values, err := creationNumbers(obj.DashPattern, len(strings.Fields(obj.DashPattern)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
total := 0.0
|
||||
for _, value := range values {
|
||||
if value < 0 {
|
||||
return "", fmt.Errorf("dash lengths must not be negative")
|
||||
return GraphicObject{}, fmt.Errorf("dash lengths must not be negative")
|
||||
}
|
||||
total += value
|
||||
}
|
||||
if total == 0 {
|
||||
return "", fmt.Errorf("dash pattern must have a positive length")
|
||||
return GraphicObject{}, fmt.Errorf("dash pattern must have a positive length")
|
||||
}
|
||||
}
|
||||
obj.ID = id
|
||||
@@ -264,45 +426,52 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
case "ImageObject":
|
||||
obj := &object.ImageObject
|
||||
if !e.images[obj.ResourceID] || (obj.ImageMask != "" && !e.images[obj.ImageMask]) {
|
||||
return "", fmt.Errorf("image resource not found")
|
||||
return GraphicObject{}, fmt.Errorf("image resource not found")
|
||||
}
|
||||
if obj.Border != nil {
|
||||
return "", fmt.Errorf("image borders are not supported for creation")
|
||||
return GraphicObject{}, fmt.Errorf("image borders are not supported for creation")
|
||||
}
|
||||
obj.ID = id
|
||||
boundary, ctm = obj.Boundary, obj.CTM
|
||||
alpha, clips, actions = obj.Alpha, obj.Clips, obj.Actions
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported object type %q", object.Type)
|
||||
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
|
||||
}
|
||||
box, err := creationBox(boundary)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
if ctm != "" {
|
||||
if _, err := creationNumbers(ctm, 6); err != nil {
|
||||
return "", fmt.Errorf("invalid CTM: %w", err)
|
||||
return GraphicObject{}, fmt.Errorf("invalid CTM: %w", err)
|
||||
}
|
||||
} else if object.Type == "ImageObject" {
|
||||
object.ImageObject.CTM = fmt.Sprintf("%s 0 0 %s 0 0", ofdNumber(box.W), ofdNumber(box.H))
|
||||
}
|
||||
if drawParam != "" || clips != nil || len(actions) != 0 {
|
||||
return "", fmt.Errorf("draw parameter references, clips and actions are not supported for creation")
|
||||
return GraphicObject{}, fmt.Errorf("draw parameter references, clips and actions are not supported for creation")
|
||||
}
|
||||
if alpha != nil && (*alpha < 0 || *alpha > 255) {
|
||||
return "", fmt.Errorf("alpha must be between 0 and 255")
|
||||
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
|
||||
}
|
||||
for _, color := range []*FillColor{fill, stroke} {
|
||||
if err := creationColor(color); err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
}
|
||||
if join != "" && join != "Miter" && join != "Round" && join != "Bevel" {
|
||||
return "", fmt.Errorf("invalid line join %q", join)
|
||||
return GraphicObject{}, fmt.Errorf("invalid line join %q", join)
|
||||
}
|
||||
data, err := objectXML(object)
|
||||
return cloneEditorObject(object)
|
||||
}
|
||||
|
||||
// cloneEditorObject 通过统一序列化规则复制对象
|
||||
// 入参: object 对象内容
|
||||
// 返回: GraphicObject 独立副本, error 错误信息
|
||||
func cloneEditorObject(object GraphicObject) (GraphicObject, error) {
|
||||
data, err := encodeOFDXML(func(x *ofdXML) { x.object(object, true) })
|
||||
if err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
object = GraphicObject{Type: object.Type}
|
||||
var target any
|
||||
@@ -315,11 +484,9 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
target = &object.ImageObject
|
||||
}
|
||||
if err := xml.Unmarshal(data, target); err != nil {
|
||||
return "", err
|
||||
return GraphicObject{}, err
|
||||
}
|
||||
e.maxID++
|
||||
e.pages[page].Content.Layer[0].Objects = append(e.pages[page].Content.Layer[0].Objects, object)
|
||||
return id, nil
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// AddText 添加单行文字,使用嵌入字体的字宽定位,不进行段落排版和复杂文字塑形
|
||||
|
||||
+30
-26
@@ -101,13 +101,11 @@ func (e *Editor) validate() error {
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
|
||||
writeXML := func(name string, encode func(*ofdXML)) error {
|
||||
var data bytes.Buffer
|
||||
x := newOFDXML(&data)
|
||||
encode(x)
|
||||
if err := x.finish(); err != nil {
|
||||
data, err := encodeOFDXML(encode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return write(name, data.Bytes(), false)
|
||||
return write(name, data, false)
|
||||
}
|
||||
if err := writeXML("OFD.xml", func(x *ofdXML) {
|
||||
x.root("OFD", ofdAttrs{{Name: xml.Name{Local: "Version"}, Value: "1.0"}, {Name: xml.Name{Local: "DocType"}, Value: "OFD"}})
|
||||
@@ -171,23 +169,7 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
|
||||
}
|
||||
for i, page := range e.pages {
|
||||
if err := writeXML(fmt.Sprintf("Doc_0/Pages/%d/Content.xml", i+1), func(x *ofdXML) {
|
||||
x.root("Page", nil)
|
||||
x.start("Area", nil)
|
||||
x.text("PhysicalBox", page.Area.PhysicalBox)
|
||||
x.end("Area")
|
||||
x.start("Content", nil)
|
||||
for _, layer := range page.Content.Layer {
|
||||
var attrs ofdAttrs
|
||||
attrs.add("ID", layer.ID)
|
||||
attrs.add("Type", layer.Type)
|
||||
x.start("Layer", attrs)
|
||||
for _, object := range layer.Objects {
|
||||
x.object(object, false)
|
||||
}
|
||||
x.end("Layer")
|
||||
}
|
||||
x.end("Content")
|
||||
x.end("Page")
|
||||
x.page(page)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -200,6 +182,28 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// page 写出页面尺寸、图层和对象
|
||||
// 入参: page 页面内容
|
||||
func (x *ofdXML) page(page PageContent) {
|
||||
x.root("Page", nil)
|
||||
x.start("Area", nil)
|
||||
x.text("PhysicalBox", page.Area.PhysicalBox)
|
||||
x.end("Area")
|
||||
x.start("Content", nil)
|
||||
for _, layer := range page.Content.Layer {
|
||||
var attrs ofdAttrs
|
||||
attrs.add("ID", layer.ID)
|
||||
attrs.add("Type", layer.Type)
|
||||
x.start("Layer", attrs)
|
||||
for _, object := range layer.Objects {
|
||||
x.object(object, false)
|
||||
}
|
||||
x.end("Layer")
|
||||
}
|
||||
x.end("Content")
|
||||
x.end("Page")
|
||||
}
|
||||
|
||||
// writeResources 写出文档资源索引
|
||||
// 入参: x XML编码器
|
||||
func (e *Editor) writeResources(x *ofdXML) {
|
||||
@@ -362,13 +366,13 @@ func (a *ofdAttrs) alpha(value *int) {
|
||||
}
|
||||
}
|
||||
|
||||
// objectXML 编码单个对象,用于隔离调用方数据并统一读写语义
|
||||
// 入参: object 图形对象
|
||||
// encodeOFDXML 编码文档节点,用于保存和独立副本
|
||||
// 入参: encode 节点写入方法
|
||||
// 返回: []byte XML数据, error 错误信息
|
||||
func objectXML(object GraphicObject) ([]byte, error) {
|
||||
func encodeOFDXML(encode func(*ofdXML)) ([]byte, error) {
|
||||
var data bytes.Buffer
|
||||
x := newOFDXML(&data)
|
||||
x.object(object, true)
|
||||
encode(x)
|
||||
if err := x.finish(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user