feat(编辑能力): 修复复合对象裁剪丢失,优化选区性能
build.yaml / build (push) Successful in 38s

This commit is contained in:
xiaoqidun committed 2026-09-17 11:51:28 +08:00
1 parent 2cc6c121f8
commit 7f79d1e70a
18 files changed
+757 -129

No files matched your search

+6 -6
View File
@@ -1647,7 +1647,7 @@ async function changeTextStyle(color) {
return;
}
if (item.items) {
if (color ? !canEditObject(item, "update") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
if (color ? !canEditObject(item, "paint") : !canEditObject(item, "layoutKnown") || !canEditObject(item, "reflow")) return;
if (!color && !el.textSize.value) return;
if (size !== item.size || fill !== null) {
await changeDocument("ofdgoStyleText", { ...item, id: item.items.map(member => member.id) }, null, color ? 0 : size, fill);
@@ -1662,7 +1662,7 @@ async function changeTextStyle(color) {
return;
}
if (size !== item.size || fill !== null) {
if (color ? !canEditObject(item, "update") : !confirmTextReflow(item)) {
if (color ? !canEditObject(item, "paint") : !confirmTextReflow(item)) {
updateObjectControls(item, true);
return;
}
@@ -1693,7 +1693,7 @@ async function changeShapeStyle({ target }) {
if (!state.editing || document.body.hasAttribute("aria-busy")) {
return;
}
if (canvasEditor.selected && (!item || !canEditObject(item, "update"))) return;
if (canvasEditor.selected && (!item || !canEditObject(item, "paint"))) return;
if (target === el.shapeWidth && (!el.shapeWidth.checkValidity() || Number(el.shapeWidth.value) <= 0)) {
el.shapeWidth.value = item ? item.lineWidth === undefined ? "" : String(displayPoints(item.lineWidth)) : "1";
return;
@@ -1741,7 +1741,7 @@ function updateDrawingControls() {
el.eraseButton.disabled = el.eraseMode.disabled = disabled;
el.eraseButton.setAttribute("aria-pressed", String(tool.startsWith("erase-")));
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
const styleDisabled = disabled || (canvasEditor.selected ? !path || !canEditObject(path, "update") : !pageCan("insert"));
const styleDisabled = disabled || (canvasEditor.selected ? !path || !canEditObject(path, "paint") : !pageCan("insert"));
el.shapeFill.disabled = el.shapeStroke.disabled = styleDisabled || line;
el.shapeFillColor.disabled = styleDisabled || line || !el.shapeFill.checked && !el.shapeFill.indeterminate;
el.shapeStrokeColor.disabled = el.shapeWidth.disabled = styleDisabled || !line && !el.shapeStroke.checked && !el.shapeStroke.indeterminate;
@@ -5478,7 +5478,7 @@ function updateObjectControls(item, reset = false) {
const textDisabled = !state.editing || !state.ready || state.exporting || Boolean(item?.items && !selection);
fontPicker.setDisabled(textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "replaceFont"));
el.textSize.disabled = textDisabled || text !== state.textDefaults && !item.draft && (!canEditObject(item, "reflow") || Boolean(item?.items) && !canEditObject(item, "layoutKnown"));
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "update");
el.textColor.disabled = textDisabled || text !== state.textDefaults && !item.draft && !canEditObject(item, "paint");
el.textFontAdd.disabled = !state.editing || !state.ready || state.exporting;
el.textAlign.disabled = el.textWrap.disabled = el.textLineHeight.disabled = el.textSpacing.disabled = el.textSize.disabled || Boolean(item?.items || item?.draft) || Boolean(canvasEditor.input);
el.paragraphButton.disabled = el.textAlign.disabled;
@@ -5516,7 +5516,7 @@ function updateObjectControls(item, reset = false) {
if (path && (reset || document.activeElement !== input)) input.value = path[key] || "#000000";
}
el.shapeWidth.placeholder = path?.items && path.lineWidth === undefined ? "混合" : "";
if (path && canEditObject(path, "update")) {
if (path && canEditObject(path, "paint")) {
if (reset || document.activeElement !== el.shapeWidth) {
el.shapeWidth.value = path.lineWidth === undefined ? "" : displayPoints(path.lineWidth);
}
+4 -2
View File
@@ -190,7 +190,9 @@ export function objectEditReason(item) {
if (!reason) return "";
let message = labels[object.capabilities.reasonCode] || "对象特性暂不支持";
if (object.capabilities.missingGlyphs) message = missingGlyphMessage(object.capabilities.missingGlyphs);
return `${object.capabilities.transform ? "部分操作受限" : "暂不可编辑"}:${message}`;
const available = object.capabilities.transform ? object.capabilities.paint ? "可移动、改色" : "可移动"
: object.capabilities.copy && object.capabilities.delete ? "可复制、删除" : "暂不可编辑";
return `${available}:${message}`;
});
return [...new Set(reasons.filter(Boolean))].join(";");
}
@@ -276,7 +278,7 @@ export class CanvasEditor {
}
node.tabIndex = 0;
node.setAttribute("role", "button");
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象" }[object.type]);
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象" }[object.type]);
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
item.artwork = artwork.get(object.id)?.node;
if (object.type === "PathObject" && (object.shape || object.outline)) {
+24 -14
View File
@@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"image"
"image/color"
"io"
"math"
"strconv"
@@ -726,6 +727,8 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
id = object.ImageObject.ID
case "PathObject":
id = object.PathObject.ID
case "CompositeObject", "CompositeGraphicUnit":
id = object.CompositeGraphicUnit.ID
default:
continue
}
@@ -749,7 +752,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
return nil, err
}
item := map[string]any{"id": id, "type": object.Type, "x": box.X, "y": box.Y, "width": box.W, "height": box.H, "order": order, "position": position.Index, "count": position.Count, "container": position.Container,
"capabilities": map[string]any{"update": capability.Update, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode)}}
"capabilities": map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode)}}
if missing := capability.MissingGlyphs; missing != nil {
item["capabilities"].(map[string]any)["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
}
@@ -761,6 +764,8 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
alpha = object.ImageObject.Alpha
case "PathObject":
alpha = object.PathObject.Alpha
case "CompositeObject", "CompositeGraphicUnit":
alpha = object.CompositeGraphicUnit.Alpha
}
item["alpha"] = 255
if alpha != nil {
@@ -780,14 +785,14 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
} else if outline, err := path.Outline(); err == nil {
item["outline"] = outline
}
if capability.Update {
if capability.Paint {
item["fill"], item["stroke"] = path.Fill != nil && *path.Fill, path.Stroke == nil || *path.Stroke
item["fillColor"] = editorColorHex(path.FillColor)
item["strokeColor"] = editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
item["lineWidth"] = path.LineWidth * editorPathScale(path)
}
}
if object.Type == "PathObject" || object.Type == "ImageObject" {
if object.Type == "PathObject" || object.Type == "ImageObject" || object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
contours, err := currentSession.Renderer.ObjectContours(object, layer.DrawParam)
if err != nil {
return nil, err
@@ -837,7 +842,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
second, _ := strconv.ParseFloat(codes[1].Y, 64)
item["lineHeight"] = second - first
}
if capability.Update {
if capability.Paint {
item["color"] = editorColorHex(object.TextObject.FillColor)
}
}
@@ -1529,10 +1534,10 @@ func setTextColor(object *ofdgo.TextObject, value string) error {
return setEditorColor(object.FillColor, value)
}
// setEditorColor 写入浏览器RGB色值,保留其他颜色属性
// 入参: color 颜色, value 十六进制色值
// setEditorColor 写入当前文档的RGB纯色,保留透明度并替换原颜色空间和索引
// 入参: fill 颜色, value 十六进制色值
// 返回: error 错误信息
func setEditorColor(color *ofdgo.FillColor, value string) error {
func setEditorColor(fill *ofdgo.FillColor, value string) error {
if len(value) != 7 || value[0] != '#' {
return fmt.Errorf("invalid RGB color %q", value)
}
@@ -1540,19 +1545,24 @@ func setEditorColor(color *ofdgo.FillColor, value string) error {
if err != nil {
return err
}
color.Value = fmt.Sprintf("%d %d %d", rgb[0], rgb[1], rgb[2])
converted, err := currentEditor.RGBColor(color.NRGBA{R: rgb[0], G: rgb[1], B: rgb[2], A: 255})
if err != nil {
return err
}
converted.Alpha = fill.Alpha
*fill = *converted
return nil
}
// editorColorHex 将创作对象的RGB颜色转换为浏览器色值
// editorColorHex 将当前文档纯色转换为浏览器色值
// 入参: color 颜色
// 返回: string 十六进制色值
func editorColorHex(color *ofdgo.FillColor) string {
var r, g, b int
if color != nil {
fmt.Sscan(color.Value, &r, &g, &b)
func editorColorHex(value *ofdgo.FillColor) string {
converted, err := currentEditor.Color(value)
if err != nil {
return ""
}
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
return fmt.Sprintf("#%02x%02x%02x", converted.R, converted.G, converted.B)
}
// editorPathScale 获取路径线宽的页面缩放比例
+5 -1
View File
@@ -63,6 +63,7 @@ type editorResource struct {
data []byte
font *Font
image *MultiMedia
space *ColorSpace
subset *editorFontSubset
}
@@ -781,7 +782,7 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
}
for _, color := range []*FillColor{fill, stroke} {
if err := creationColor(color); err != nil {
if err := e.editorColor(color); err != nil {
return GraphicObject{}, &EditError{Code: EditUnsupportedColor, Err: err}
}
}
@@ -890,6 +891,9 @@ func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) err
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
// 返回: GraphicObject 变换后的对象, error 错误信息
func transformEditorObject(object GraphicObject, dx, dy, scale float64) (GraphicObject, error) {
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" || object.Type == "TextObject" && (len(object.TextObject.CGTransform) != 0 || object.TextObject.VScale != 0 || object.TextObject.Decoration != "") {
return transformEditorMatrix(object, Matrix{a: scale, d: scale, e: dx, f: dy}), nil
}
var boundary, ctm *string
switch object.Type {
case "TextObject":
+27 -18
View File
@@ -82,18 +82,14 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
if len(objects) == 0 {
return nil, nil
}
if !e.sourceRGB() {
return nil, fmt.Errorf("inserting objects requires an RGB default color space")
}
if err := e.prepareSourceIDs(); err != nil {
return nil, err
}
prepared := make([]GraphicObject, len(objects))
result := make([]string, len(objects))
for i, object := range objects {
origin := e.objectOrigin(editorObjectID(object))
result[i] = strconv.Itoa(e.maxID + i + 1)
object, err := e.prepareObject(result[i], object)
object, err := e.prepareCopiedObject(result[i], object)
if err != nil {
return nil, err
}
@@ -102,14 +98,9 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
if err != nil {
return nil, err
}
codes := object.TextObject.TextCode
object, err = e.prepareObject(result[i], object)
if err != nil {
if err = validateEditorGeometry(object); err != nil {
return nil, err
}
if origin != nil {
object.TextObject.TextCode = codes
}
}
prepared[i] = object
}
@@ -308,7 +299,7 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
if err != nil {
return err
}
object, err = transformEditorObject(object, dx, dy, 1)
object, err = e.transformObject(object, dx, dy, 1)
if err != nil {
return err
}
@@ -356,16 +347,27 @@ func (e *Editor) updateObjects(page int, objects []GraphicObject, preserveConten
}
after := make([]GraphicObject, len(objects))
for i, object := range objects {
if e.originalPage(page) {
preserved := preserveContent
origin := e.objectOrigin(ids[i])
if origin != nil {
capability, err := e.ObjectCapabilities(page, ids[i])
if err != nil {
return err
}
if !capability.Transform || !preserveContent && !e.sourceRGB() {
if !capability.Transform {
return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError())
}
if !preserved && !capability.Update && editorPreservedObject(before[i], object) && capability.Paint {
if err := e.validatePreservedAppearance(before[i], object); err != nil {
return err
}
preserved = true
}
if !preserved && !editorXMLSupported(origin.node) {
return capability.editError()
}
}
if preserveContent && e.originalPage(page) {
if preserved && origin != nil {
if err := validateEditorGeometry(object); err != nil {
return err
}
@@ -423,7 +425,7 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
if err != nil {
return err
}
objects[i], err = transformEditorObject(object, dx, dy, scale)
objects[i], err = e.transformObject(object, dx, dy, scale)
if err != nil {
return err
}
@@ -476,7 +478,7 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
if err != nil {
return err
}
objects[i], err = transformEditorObject(objects[i], dx, dy, 1)
objects[i], err = e.transformObject(objects[i], dx, dy, 1)
if err != nil {
return err
}
@@ -576,10 +578,17 @@ func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error)
return nil, err
}
defer reader.Close()
if _, err := reader.PageContentByIndex(page); err != nil {
return nil, err
}
renderer := NewRenderer(reader)
boxes := make([]Box, len(objects))
for i, object := range objects {
boxes[i], err = renderer.ObjectBounds(object, "")
layer, _, findErr := e.findObject(page, editorObjectID(object))
if findErr != nil {
return nil, findErr
}
boxes[i], err = renderer.ObjectBounds(object, layer.DrawParam)
if err != nil {
return nil, err
}
+171
View File
@@ -0,0 +1,171 @@
// 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"
"image/color"
"maps"
"slices"
"strconv"
"strings"
)
// Color 解析当前文档的纯色,支持RGB、灰度、CMYK及调色板,不修改原颜色
// 入参: value 颜色,nil表示默认黑色
// 返回: color.NRGBA 非预乘颜色, error 错误信息
func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
space := ColorSpace{Type: "RGB", BitsPerComponent: 8}
if value == nil {
return color.NRGBA{A: 255}, nil
}
if value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.unsupported {
return color.NRGBA{}, fmt.Errorf("color is not a supported solid color")
}
id := value.ColorSpace
if id == "" && e.source != nil && e.source.document.CommonData.DefaultCS != 0 {
id = strconv.Itoa(e.source.document.CommonData.DefaultCS)
}
if id != "" {
var found *ColorSpace
if e.source != nil {
found = e.source.reader.colorSpaceCache[id]
}
for _, resource := range e.resources {
if resource.space != nil && resource.space.ID == id {
found = resource.space
break
}
}
if found == nil {
return color.NRGBA{}, fmt.Errorf("color space %q not found", id)
}
space = *found
}
bits := space.BitsPerComponent
if bits == 0 {
bits = 8
}
if bits != 1 && bits != 2 && bits != 4 && bits != 8 && bits != 16 {
return color.NRGBA{}, fmt.Errorf("unsupported color depth %d", bits)
}
count := 3
switch space.Type {
case "RGB":
case "GRAY":
count = 1
case "CMYK":
count = 4
default:
return color.NRGBA{}, fmt.Errorf("unsupported color space %q", space.Type)
}
text := value.Value
if strings.TrimSpace(text) == "" && value.Index != nil {
if *value.Index < 0 || *value.Index >= len(space.Palette) {
return color.NRGBA{}, fmt.Errorf("color palette index out of range")
}
text = space.Palette[*value.Index]
}
fields := strings.Fields(text)
if len(fields) != count {
return color.NRGBA{}, fmt.Errorf("expected %d color components", count)
}
var components [4]uint8
limit := 1<<bits - 1
for i, field := range fields {
v, err := parseColorComponent(field)
if err != nil || v < 0 || v > limit {
return color.NRGBA{}, fmt.Errorf("invalid color component %q", field)
}
components[i] = uint8((v*255 + limit/2) / limit)
}
r, g, b := components[0], components[1], components[2]
if space.Type == "GRAY" {
g, b = r, r
} else if space.Type == "CMYK" {
r, g, b = color.CMYKToRGB(components[0], components[1], components[2], components[3])
}
a := 255
if value.Alpha != nil {
a = *value.Alpha
if a < 0 || a > 255 {
return color.NRGBA{}, fmt.Errorf("color alpha must be between 0 and 255")
}
}
return color.NRGBA{R: r, G: g, B: b, A: uint8(a)}, nil
}
// RGBColor 创建浏览器等调用方使用的RGB纯色,非RGB文档按需注册独立颜色空间
// 不修改文档默认颜色空间,未使用的新增颜色空间不写出
// 入参: value 非预乘颜色
// 返回: *FillColor OFD颜色, error 错误信息
func (e *Editor) RGBColor(value color.NRGBA) (*FillColor, error) {
result := &FillColor{Value: fmt.Sprintf("%d %d %d", value.R, value.G, value.B)}
if value.A != 255 {
alpha := int(value.A)
result.Alpha = &alpha
}
if e.sourceRGB() {
return result, nil
}
for _, id := range slices.Sorted(maps.Keys(e.source.reader.colorSpaceCache)) {
space := e.source.reader.colorSpaceCache[id]
if space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0 {
result.ColorSpace = id
return result, nil
}
}
for _, resource := range e.resources {
if resource.space != nil {
result.ColorSpace = resource.space.ID
return result, nil
}
}
if err := e.prepareSourceIDs(); err != nil {
return nil, err
}
result.ColorSpace = e.nextID()
e.resources = append(e.resources, editorResource{space: &ColorSpace{ID: result.ColorSpace, Type: "RGB", BitsPerComponent: 8}})
return result, nil
}
// editorPaintable 判断文字与路径颜色能否在保留其他字段的前提下修改
// 入参: object 图形对象
// 返回: bool 是否支持纯色编辑
func (e *Editor) editorPaintable(object GraphicObject) bool {
var fill, stroke *FillColor
switch object.Type {
case "TextObject":
fill, stroke = object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor)
case "PathObject":
fill, stroke = object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)
default:
return false
}
for _, value := range []*FillColor{fill, stroke} {
if _, err := e.Color(value); err != nil {
return false
}
}
return true
}
// editorColor 校验当前文档的颜色空间和分量
// 入参: value 颜色
// 返回: error 错误信息
func (e *Editor) editorColor(value *FillColor) error {
_, err := e.Color(value)
return err
}
+3 -1
View File
@@ -184,7 +184,7 @@ func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, r
}
// editorObjectClips 获取基本对象的裁剪字段
// 入参: object 文字、路径或图片对象
// 入参: object 文字、路径、图片或复合对象
// 返回: **Clips 裁剪字段地址,不支持的对象返回nil
func editorObjectClips(object *GraphicObject) **Clips {
switch object.Type {
@@ -194,6 +194,8 @@ func editorObjectClips(object *GraphicObject) **Clips {
return &object.PathObject.Clips
case "ImageObject":
return &object.ImageObject.Clips
case "CompositeObject", "CompositeGraphicUnit":
return &object.CompositeGraphicUnit.Clips
}
return nil
}
+229 -32
View File
@@ -60,9 +60,11 @@ type editorSourcePage struct {
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
type ObjectCapabilities struct {
Update bool
Paint bool
ReplaceFont bool
Reflow bool
LayoutKnown bool
@@ -128,9 +130,9 @@ func (r *Reader) Editor() (*Editor, error) {
}
e := NewEditor()
e.Info, e.maxID = cloneEditorData(*info), maximum
directory := path.Join(reader.RootDir, "Edit")
directory := path.Join(cleanPackagePath(reader.RootDir), "Edit")
for i := 1; editorDirectoryExists(reader, directory); i++ {
directory = path.Join(reader.RootDir, "Edit_"+strconv.Itoa(i))
directory = path.Join(cleanPackagePath(reader.RootDir), "Edit_"+strconv.Itoa(i))
}
e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), directory: directory, pages: make(map[string]*editorSourcePage), origins: make(map[string]*editorObjectOrigin)}
for _, page := range doc.Pages.Page {
@@ -322,7 +324,7 @@ func (e *Editor) loadSourcePage(index int) error {
if node := nodes[id]; node != nil {
origin := &editorObjectOrigin{page: source, node: node, object: object}
e.source.origins[id] = origin
if editorXMLSupported(node) {
if editorXMLTransformable(node) {
resolved, err := e.resolveEditorStyle(object, layer.DrawParam)
if err != nil {
origin.reason = err
@@ -339,7 +341,7 @@ func (e *Editor) loadSourcePage(index int) error {
}
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制
// 字体缺失或子集不包含原文时仍可保真移动、复制和删除,不自动重新排版
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项
// 入参: page 页面索引, id 对象标识
// 返回: ObjectCapabilities 操作能力, error 错误信息
@@ -349,47 +351,51 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
return ObjectCapabilities{}, err
}
object := layer.Objects[index]
all := ObjectCapabilities{Update: true, ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
if !e.originalPage(page) {
all := ObjectCapabilities{Update: true, Paint: object.Type == "TextObject" || object.Type == "PathObject", ReplaceFont: object.Type == "TextObject", Reflow: object.Type == "TextObject" && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0, LayoutKnown: object.Type == "TextObject" && (e.objectOrigin(id) == nil || object.TextObject.layout != nil), Transform: true, Arrange: true, Copy: true, Delete: true, Order: true}
origin := e.objectOrigin(id)
if origin == nil {
return all, nil
}
source := e.source.pages[e.pages[page].ID]
node := source.nodes[id]
if node == nil {
return all, nil
}
if !editorXMLSupported(node) {
node := origin.node
if !editorXMLTransformable(node) {
return ObjectCapabilities{Reason: "object uses unsupported editing features", ReasonCode: EditUnsupportedObject}, nil
}
if origin := e.objectOrigin(id); origin.reason != nil {
return ObjectCapabilities{Reason: origin.reason.Error(), ReasonCode: editReason(origin.reason)}, nil
if origin.reason != nil && editReason(origin.reason) == EditUnsupportedStyle {
return ObjectCapabilities{Reason: origin.reason.Error(), ReasonCode: EditUnsupportedStyle}, nil
}
for parent := node.parent; parent.name.Local != "Content"; parent = parent.parent {
allowed := "ID"
if parent.name.Local == "Layer" {
allowed += " Type DrawParam"
}
if !editorXMLAttributes(parent, allowed) {
return ObjectCapabilities{Reason: "object container uses unsupported editing features", ReasonCode: EditUnsupportedContainer}, nil
}
if !editorXMLContainersSupported(node) {
return ObjectCapabilities{Reason: "object container uses unsupported editing features", ReasonCode: EditUnsupportedContainer}, nil
}
if err := validateEditorGeometry(object); err != nil {
return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject}, nil
}
all.Order = editorContainerOrderable(node.parent)
if _, err := e.prepareObject(id, object); err != nil {
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, err.Error()
all.Copy = editorXMLCopyable(node) && origin.reason == nil
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
for parent := node.parent; parent != nil; parent = parent.parent {
if parent.name.Local == "Layer" && parent.attr("DrawParam") != "" {
all.Copy = false
}
}
}
if origin.reason != nil {
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
all.Reason, all.ReasonCode = origin.reason.Error(), editReason(origin.reason)
} else if !editorXMLSupported(node) {
all.Update, all.Reflow, all.ReplaceFont = false, false, false
all.Reason, all.ReasonCode = "content can only be edited with its original structure preserved", EditUnsupportedObject
} else if _, err := e.prepareObject(id, object); err != nil {
all.Update, all.Reflow, all.Reason = false, false, err.Error()
all.ReasonCode = editReason(err)
errors.As(err, &all.MissingGlyphs)
}
if !e.sourceRGB() {
all.ReplaceFont = false
all.Update, all.Reflow, all.Copy, all.Reason = false, false, false, "document uses a non-RGB default color space"
all.MissingGlyphs = nil
all.ReasonCode = EditUnsupportedColor
all.Paint = all.Paint && e.editorPaintable(object)
if object.Type == "ImageObject" && object.ImageObject.Border != nil {
all.Transform, all.Arrange = false, false
all.Reason, all.ReasonCode = "image border transformations are not supported", EditUnsupportedObject
}
if object.Type == "TextObject" {
all.Arrange = all.Update
all.Arrange = all.Update || e.editorTextMeasurable(object.TextObject)
if _, err := object.TextObject.TextFrame(); err != nil {
all.Reflow = false
}
@@ -407,12 +413,147 @@ func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
return e.source.origins[id]
}
// editorPreservedObject 判断对象是否仅改变标识、几何和可独立写回的外观字段
// 入参: before 原始快照, after 新快照
// 返回: bool 是否保留原内容
func editorPreservedObject(before, after GraphicObject) bool {
if before.Type != after.Type {
return false
}
before = cloneEditorData(before)
switch before.Type {
case "TextObject":
a, b := &before.TextObject, after.TextObject
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
case "PathObject":
a, b := &before.PathObject, after.PathObject
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
case "ImageObject":
a, b := &before.ImageObject, after.ImageObject
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
case "CompositeObject", "CompositeGraphicUnit":
a, b := &before.CompositeGraphicUnit, after.CompositeGraphicUnit
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
default:
return false
}
a, b := *editorObjectClips(&before), *editorObjectClips(&after)
if a == nil {
*editorObjectClips(&before) = b
} else if b != nil && len(a.Clip) <= len(b.Clip) {
a.Clip = append(a.Clip, b.Clip[len(a.Clip):]...)
for i := range a.Clip {
if len(a.Clip[i].Area) == len(b.Clip[i].Area) {
for j := range a.Clip[i].Area {
a.Clip[i].Area[j].CTM = b.Clip[i].Area[j].CTM
}
}
}
}
return reflect.DeepEqual(before, after)
}
// prepareCopiedObject 保真复制原对象,不重新生成字形或丢弃未修改字段
// 入参: id 新标识, object 对象快照
// 返回: GraphicObject 独立副本, error 错误信息
func (e *Editor) prepareCopiedObject(id string, object GraphicObject) (GraphicObject, error) {
origin := e.objectOrigin(editorObjectID(object))
if origin == nil {
return e.prepareObject(id, object)
}
if !editorXMLTransformable(origin.node) || !editorXMLContainersSupported(origin.node) || !editorXMLCopyable(origin.node) || origin.reason != nil {
return GraphicObject{}, fmt.Errorf("object cannot be copied without changing its original structure")
}
var drawParam string
for parent := origin.node.parent; parent != nil; parent = parent.parent {
if parent.name.Local == "Layer" {
drawParam = parent.attr("DrawParam")
break
}
}
before, err := e.resolveEditorStyle(origin.object, drawParam)
if err != nil {
return GraphicObject{}, err
}
if drawParam != "" && (object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit") {
return GraphicObject{}, fmt.Errorf("copy requires preserving the composite layer draw parameters")
}
if !editorPreservedObject(before, object) {
if !editorXMLSupported(origin.node) {
return GraphicObject{}, fmt.Errorf("copy would replace unsupported object content")
}
return e.prepareObject(id, object)
}
if err := validateEditorGeometry(object); err != nil {
return GraphicObject{}, err
}
if err := e.validatePreservedAppearance(before, object); err != nil {
return GraphicObject{}, err
}
result := cloneEditorData(object)
switch result.Type {
case "TextObject":
result.TextObject.ID = id
case "PathObject":
result.PathObject.ID = id
case "ImageObject":
result.ImageObject.ID = id
case "CompositeObject", "CompositeGraphicUnit":
result.CompositeGraphicUnit.ID = id
}
return result, nil
}
// validatePreservedAppearance 校验保真对象实际修改的颜色、描边和裁剪,不重新解释未修改字段
// 入参: before 原对象, after 新对象
// 返回: error 错误信息
func (e *Editor) validatePreservedAppearance(before, after GraphicObject) error {
old := []*FillColor{before.TextObject.FillColor, (*FillColor)(before.TextObject.StrokeColor), before.PathObject.FillColor, (*FillColor)(before.PathObject.StrokeColor)}
next := []*FillColor{after.TextObject.FillColor, (*FillColor)(after.TextObject.StrokeColor), after.PathObject.FillColor, (*FillColor)(after.PathObject.StrokeColor)}
for i, value := range next {
if !reflect.DeepEqual(old[i], value) {
if err := e.editorColor(value); err != nil {
return err
}
}
}
a, b := *editorObjectClips(&before), *editorObjectClips(&after)
if b != nil {
count := 0
if a != nil {
count = len(a.Clip)
}
for i, clip := range b.Clip {
if i >= count {
if err := e.validateObjectClips(&Clips{Clip: []Clip{clip}}); err != nil {
return err
}
continue
}
for j, area := range clip.Area {
if area.CTM != a.Clip[i].Area[j].CTM && area.CTM != "" {
if _, err := creationNumbers(area.CTM, 6); err != nil {
return err
}
}
}
}
}
if after.Type == "PathObject" {
return validateEditorStroke(after.PathObject)
}
return nil
}
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序
// 入参: container 图层或页块
// 返回: bool 是否可排序
func editorContainerOrderable(container *editorXML) bool {
for _, child := range container.children {
if !editorXMLSupported(child) {
if !editorXMLTransformable(child) {
return false
}
}
@@ -430,6 +571,8 @@ func editorGeometry(object GraphicObject) (string, string) {
return object.PathObject.Boundary, object.PathObject.CTM
case "ImageObject":
return object.ImageObject.Boundary, object.ImageObject.CTM
case "CompositeObject", "CompositeGraphicUnit":
return object.CompositeGraphicUnit.Boundary, object.CompositeGraphicUnit.CTM
}
return "", ""
}
@@ -456,6 +599,46 @@ func (e *Editor) editorFont(id string) (*font.SFNT, error) {
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("embedded font %q not found", id)}
}
// editorTextMeasurable 判断原字形能否在不使用回退字体的情况下度量
// 入参: text 原文字对象
// 返回: bool 是否可准确度量
func (e *Editor) editorTextMeasurable(text TextObject) bool {
sfnt, err := e.editorFont(text.Font)
if err != nil {
return false
}
transforms := make(map[int]CGTransform, len(text.CGTransform))
for _, transform := range text.CGTransform {
transforms[transform.CodePosition] = transform
}
offset := 0
for _, code := range text.TextCode {
runes := textCodeRunes(code.Value)
for i := 0; i < len(runes); {
if transform, ok := transforms[offset+i]; ok {
ids := parseInts(transform.Glyphs)
count := max(1, transform.CodeCount)
if len(ids) == 0 || count > len(runes)-i {
return false
}
for _, id := range ids {
if id <= 0 || id >= int(sfnt.NumGlyphs()) {
return false
}
}
i += count
} else {
if sfnt.GlyphIndex(runes[i]) == 0 {
return false
}
i++
}
}
offset += len(runes)
}
return true
}
// editorImage 按需读取原图片尺寸,不解码完整像素
// 入参: id 图片资源标识
// 返回: image.Point 图片尺寸, error 错误信息
@@ -541,9 +724,23 @@ func validateEditorGeometry(object GraphicObject) error {
return err
}
}
var alpha *int
switch object.Type {
case "TextObject":
alpha = object.TextObject.Alpha
case "PathObject":
alpha = object.PathObject.Alpha
case "ImageObject":
alpha = object.ImageObject.Alpha
case "CompositeObject", "CompositeGraphicUnit":
alpha = object.CompositeGraphicUnit.Alpha
}
if alpha != nil && (*alpha < 0 || *alpha > 255) {
return fmt.Errorf("alpha must be between 0 and 255")
}
if object.Type == "TextObject" {
obj := object.TextObject
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.LineWidth) {
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.LineWidth) || obj.LineWidth < 0 {
return fmt.Errorf("invalid text geometry")
}
for _, code := range obj.TextCode {
+3 -3
View File
@@ -66,17 +66,17 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
return nil, err
}
fonts, images, resourceFiles := e.usedResources()
fonts, images, spaces, resourceFiles := e.usedResources()
commonData := source.document.CommonData
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
return strings.EqualFold(file, reader.ResPath(name))
})
}
if len(fonts)+len(images) != 0 {
if len(fonts)+len(images)+len(spaces) != 0 {
resourcePath := path.Join(source.directory, "Resources.xml")
resourceFiles = append(resourceFiles, resourcePath)
data, err := encodeOFDXML(func(x *ofdXML) { x.resources(fonts, images) })
data, err := encodeOFDXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
if err != nil {
return nil, err
}
+7 -3
View File
@@ -87,7 +87,7 @@ func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (Graphic
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
}
for _, color := range []*FillColor{style.FillColor, (*FillColor)(style.StrokeColor)} {
if err := creationColor(color); err != nil {
if err := e.editorColor(color); err != nil {
return GraphicObject{}, &EditError{Code: EditUnsupportedColor, Err: fmt.Errorf("unsupported draw parameter color: %w", err)}
}
}
@@ -103,7 +103,7 @@ type ObjectStyle struct {
Join *string
}
// StyleObjects 原子更新文字、图片和路径透明度,路径另支持描边样式
// StyleObjects 原子更新文字、图片、路径与复合对象透明度,路径另支持描边样式
// 入参: page 页面索引, ids 对象标识, style 待修改属性
// 返回: error 错误信息
func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
@@ -129,6 +129,10 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
if style.Alpha != nil {
object.ImageObject.Alpha = style.Alpha
}
case "CompositeObject", "CompositeGraphicUnit":
if style.Alpha != nil {
object.CompositeGraphicUnit.Alpha = style.Alpha
}
case "PathObject":
path := &object.PathObject
if style.Alpha != nil {
@@ -208,7 +212,7 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) error {
return err
}
for _, color := range []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)} {
if err := creationColor(color); err != nil {
if err := e.editorColor(color); err != nil {
return err
}
}
+13 -3
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"fmt"
"image/color"
"strings"
"github.com/go-text/typesetting/segmenter"
@@ -125,10 +126,19 @@ func (e *Editor) styleTextObjects(objects []GraphicObject, style TextStyle) ([]G
}
}
if style.Color != "" {
if text.FillColor == nil {
text.FillColor = &FillColor{}
if err := creationColor(&FillColor{Value: style.Color}); err != nil {
return nil, err
}
text.FillColor.Value = style.Color
var r, g, b float64
fmt.Sscan(style.Color, &r, &g, &b)
value, err := e.RGBColor(color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255})
if err != nil {
return nil, err
}
if text.FillColor != nil {
value.Alpha = text.FillColor.Alpha
}
text.FillColor = value
}
}
return updates, nil
+13 -1
View File
@@ -93,7 +93,7 @@ func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
for i, object := range objects {
object = cloneEditorData(object)
if uniform {
object, err = transformEditorObject(object, matrix.e, matrix.f, sx)
object, err = e.transformObject(object, matrix.e, matrix.f, sx)
} else if object.Type == "ImageObject" {
object = transformEditorMatrix(object, matrix)
} else {
@@ -107,6 +107,16 @@ func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
return e.updateObjects(page, objects, true)
}
// transformObject 对原有复杂对象仅更新变换,不重建其文字、画刷或其他局部属性
// 入参: object 对象, dx、dy 页面位移, scale 缩放比例
// 返回: GraphicObject 新对象, error 错误信息
func (e *Editor) transformObject(object GraphicObject, dx, dy, scale float64) (GraphicObject, error) {
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && (!editorXMLSupported(origin.node) || origin.reason != nil) {
return transformEditorMatrix(object, Matrix{a: scale, d: scale, e: dx, f: dy}), nil
}
return transformEditorObject(object, dx, dy, scale)
}
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片
// 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵
// 返回: error 错误信息
@@ -143,6 +153,8 @@ func transformEditorMatrix(object GraphicObject, matrix Matrix) GraphicObject {
boundary, ctm = &object.PathObject.Boundary, &object.PathObject.CTM
case "ImageObject":
boundary, ctm = &object.ImageObject.Boundary, &object.ImageObject.CTM
case "CompositeObject", "CompositeGraphicUnit":
boundary, ctm = &object.CompositeGraphicUnit.Boundary, &object.CompositeGraphicUnit.CTM
}
before, _ := ParseBox(*boundary)
if object.Type == "ImageObject" && *ctm == "" {
+149 -10
View File
@@ -244,7 +244,7 @@ func editorXMLSupported(node *editorXML) bool {
case "TextCode":
allowed = "X Y DeltaX DeltaY"
case "FillColor", "StrokeColor":
allowed = "Value Alpha"
allowed = "Value Alpha ColorSpace Index"
case "Clips":
allowed, children = "TransFlag", "Clip"
case "Clip":
@@ -266,6 +266,54 @@ func editorXMLSupported(node *editorXML) bool {
return true
}
// editorXMLTransformable 判断对象是否支持保留原文的几何操作
// 入参: node 原始对象节点
// 返回: bool 是否支持
func editorXMLTransformable(node *editorXML) bool {
var attrs string
switch node.name.Local {
case "TextObject":
attrs = "ID Boundary CTM DrawParam Font Size HScale VScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Decoration Visible Stroke Fill Alpha"
case "PathObject":
attrs = "ID Boundary CTM DrawParam LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha"
case "ImageObject":
attrs = "ID Boundary CTM ResourceID ImageMask Visible Alpha"
case "CompositeObject", "CompositeGraphicUnit":
attrs = "ID Boundary CTM ResourceID DrawParam Visible Alpha"
default:
return false
}
return editorXMLAttributes(node, attrs)
}
// editorXMLContainersSupported 判断父级容器是否仅含可保留的标识和样式
// 入参: node 对象节点
// 返回: bool 是否支持容器内操作
func editorXMLContainersSupported(node *editorXML) bool {
for parent := node.parent; parent.name.Local != "Content"; parent = parent.parent {
allowed := "ID"
if parent.name.Local == "Layer" {
allowed += " Type DrawParam"
}
if !editorXMLAttributes(parent, allowed) {
return false
}
}
return true
}
// editorXMLCopyable 判断独立复制是否无需重写内部标识或动作引用
// 入参: node 原始对象节点
// 返回: bool 是否支持保真复制
func editorXMLCopyable(node *editorXML) bool {
for _, child := range node.children {
if child.attr("ID") != "" || child.name.Local == "Actions" || !editorXMLCopyable(child) {
return false
}
}
return true
}
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
@@ -274,7 +322,7 @@ func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject)
if before.Type != after.Type {
return editorObjectXML(after)
}
if node.attr("DrawParam") != "" && before.TextObject.DrawParam == "" && before.PathObject.DrawParam == "" {
if node.attr("DrawParam") != "" && (before.Type == "TextObject" && before.TextObject.DrawParam == "" || before.Type == "PathObject" && before.PathObject.DrawParam == "") {
before = GraphicObject{Type: before.Type}
var err error
switch before.Type {
@@ -342,9 +390,6 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
}
}
}
if contentChanged && attr.Name.Space == "xmlns" && attr.Name.Local == "ofd" {
continue
}
key := attr.Name.Local
if attr.Name.Space != "" {
key = attr.Name.Space + ":" + key
@@ -360,15 +405,12 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
result.WriteByte('"')
}
}
if contentChanged {
result.WriteString(" xmlns:ofd=\"" + ofdNamespace + "\"")
}
result.WriteByte('>')
if contentChanged {
matching := len(node.children) > 0 && len(node.children) == len(oldNode.children) && len(node.children) == len(newNode.children)
if matching {
for i, child := range node.children {
if child.name.Local != oldNode.children[i].name.Local || child.name.Local != newNode.children[i].name.Local {
if child.name.Space != "" && child.name.Space != ofdNamespace && child.name.Space != "http://www.ofdspec.org" || child.name.Local != oldNode.children[i].name.Local || child.name.Local != newNode.children[i].name.Local {
matching = false
break
}
@@ -388,7 +430,11 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
}
result.Write(data[position:node.close])
} else {
result.Write(newXML[newNode.open:newNode.close])
content, err := editorXMLMergeContent(data, node, oldXML, oldNode, newXML, newNode)
if err != nil {
return nil, err
}
result.Write(content)
}
} else if node.open != node.end {
result.Write(data[node.open:node.close])
@@ -397,6 +443,99 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
return result.Bytes(), nil
}
// editorXMLMergeContent 按字段合并子节点,保留未修改字段、扩展节点和注释
// 入参: data、node 原文节点, oldXML、oldNode 修改前编码, newXML、newNode 修改后编码
// 返回: []byte 子节点内容, error 错误信息
func editorXMLMergeContent(data []byte, node *editorXML, oldXML []byte, oldNode *editorXML, newXML []byte, newNode *editorXML) ([]byte, error) {
if len(oldNode.children) == 0 && len(newNode.children) == 0 {
return newXML[newNode.open:newNode.close], nil
}
groups := func(root *editorXML, encoded bool) map[string][]*editorXML {
result := make(map[string][]*editorXML)
for _, child := range root.children {
if encoded || child.name.Space == "" || child.name.Space == ofdNamespace || child.name.Space == "http://www.ofdspec.org" {
result[child.name.Local] = append(result[child.name.Local], child)
}
}
return result
}
original, before, after := groups(node, false), groups(oldNode, true), groups(newNode, true)
var names []string
for _, child := range append(slices.Clone(newNode.children), oldNode.children...) {
if !slices.Contains(names, child.name.Local) {
names = append(names, child.name.Local)
}
}
var patches []editorXMLPatch
for _, name := range names {
old, next, source := before[name], after[name], original[name]
var oldData, newData []byte
for _, child := range old {
oldData = append(oldData, oldXML[child.start:child.end]...)
}
for _, child := range next {
newData = append(newData, newXML[child.start:child.end]...)
}
if bytes.Equal(oldData, newData) {
continue
}
if len(source) > 0 && len(source) == len(old) {
for i, child := range source {
var encoded []byte
if i < len(next) {
var err error
encoded, err = editorXMLMerge(data, child, oldXML[old[i].start:old[i].end], newXML[next[i].start:next[i].end])
if err != nil {
return nil, err
}
}
patches = append(patches, editorXMLPatch{child.start - node.open, child.end - node.open, encoded})
}
if len(next) > len(source) {
var added []byte
for _, child := range next[len(source):] {
added = append(added, editorXMLEncodedFragment(newXML, child)...)
}
position := source[len(source)-1].end - node.open
patches = append(patches, editorXMLPatch{position, position, added})
}
continue
}
newData = nil
for _, child := range next {
newData = append(newData, editorXMLEncodedFragment(newXML, child)...)
}
if len(source) > 0 {
for i, child := range source {
var encoded []byte
if i == 0 {
encoded = newData
}
patches = append(patches, editorXMLPatch{child.start - node.open, child.end - node.open, encoded})
}
} else if len(next) > 0 {
position := node.close
for _, child := range newNode.children {
if child.start > next[0].start && len(original[child.name.Local]) > 0 {
position = original[child.name.Local][0].start
break
}
}
patches = append(patches, editorXMLPatch{position - node.open, position - node.open, newData})
}
}
return editorPatchXML(data[node.open:node.close], patches), nil
}
// editorXMLEncodedFragment 为新增的标准编码节点局部声明前缀,不覆盖原文命名空间
// 入参: data 标准编码内容, node 待插入的子节点
// 返回: []byte 独立片段
func editorXMLEncodedFragment(data []byte, node *editorXML) []byte {
position := node.open - node.start - 1
fragment := data[node.start:node.end]
return editorPatchXML(fragment, []editorXMLPatch{{position, position, []byte(" xmlns:ofd=\"" + ofdNamespace + "\"")}})
}
// editorXMLContainer 用标准编码器封装已编码的子节点
// 入参: name 节点名称, attrs 属性, content 子节点XML
// 返回: []byte XML片段, error 错误信息
+2 -1
View File
@@ -220,6 +220,7 @@ func (r *Reader) loadRes(resPath string) {
for i := range res.ColorSpaces.ColorSpace {
cs := &res.ColorSpaces.ColorSpace[i]
r.colorSpaceCache[cs.ID] = cs
r.resourceFiles[cs.ID] = fullPath
}
for _, mm := range res.MultiMedias.MultiMedia {
if mm.MediaFile != "" {
@@ -456,7 +457,7 @@ func (r *Reader) ResPath(resLink string) string {
if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) {
return resLink
}
return path.Join(r.RootDir, resLink)
return cleanPackagePath(path.Join(r.RootDir, resLink))
}
// ResData 获取资源文件数据
+39 -24
View File
@@ -21,7 +21,7 @@ import (
"github.com/tdewolff/canvas"
)
// ObjectBounds 获取文字、路径或图片在页面坐标中的轴对齐范围,不修改对象
// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
// 底纹按填充或描边轮廓度量,不展开图案单元
// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误
@@ -37,13 +37,13 @@ type ObjectContour struct {
EvenOdd bool `json:"evenOdd,omitempty"`
}
// ObjectContours 获取路径或图片的实际绘制区域,包含描边、虚线、变换及对象裁剪
// ObjectContours 获取路径、图片或复合对象的实际绘制区域,包含描边、虚线、变换及对象裁剪
// 图片不解码像素,底纹不展开图案单元;不包含页面边界或父级变换
// 入参: object 路径或图片对象, drawParam 图层绘制参数标识
// 入参: object 路径、图片或复合对象, drawParam 图层绘制参数标识
// 返回: []ObjectContour 可用于点选的轮廓, error 错误信息
func (r *Renderer) ObjectContours(object GraphicObject, drawParam string) ([]ObjectContour, error) {
if object.Type != "PathObject" && object.Type != "ImageObject" {
return nil, fmt.Errorf("contours require a path or image object")
if object.Type != "PathObject" && object.Type != "ImageObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
return nil, fmt.Errorf("contours require a path, image or composite object")
}
bounds := &boundsRenderer{collect: true}
_, err := r.measureObject(object, drawParam, bounds)
@@ -54,7 +54,7 @@ func (r *Renderer) ObjectContours(object GraphicObject, drawParam string) ([]Obj
// 入参: object 对象, drawParam 图层绘制参数标识, bounds 度量目标
// 返回: Box 毫米范围, error 错误信息
func (r *Renderer) measureObject(object GraphicObject, drawParam string, bounds *boundsRenderer) (Box, error) {
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" {
if object.Type != "TextObject" && object.Type != "PathObject" && object.Type != "ImageObject" && object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
return Box{}, fmt.Errorf("unsupported object type %q", object.Type)
}
boundary, ctm := editorGeometry(object)
@@ -97,28 +97,43 @@ func (r *Renderer) measureObject(object GraphicObject, drawParam string, bounds
obj.StrokeColor = (*StrokeColor)(boundsColor((*FillColor)(obj.StrokeColor)))
renderer.renderPath(ctx, obj, 0, defaults, nil, false, nil)
case "ImageObject":
obj := object.ImageObject
if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
return Box{}, nil
}
box, _ := ParseBox(obj.Boundary)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{ctm.a, ctm.c, box.X + ctm.e}, {-ctm.b, -ctm.d, -box.Y - ctm.f}})
clip := renderer.buildObjectClipPath(obj.Clips, 0, box.X, box.Y, ctm, nil, false)
bounds.add(applyClipPath(p, clip))
if obj.Border != nil {
border := *obj.Border
border.BorderColor = (*StrokeColor)(boundsColor((*FillColor)(border.BorderColor)))
obj.Border = &border
renderer.renderImageBorder(ctx, obj, box, 0, nil, false, clip)
}
renderer.measureImage(ctx, object.ImageObject, 0, nil, false, nil)
case "CompositeObject", "CompositeGraphicUnit":
renderer.renderCompositeGraphicUnit(ctx, object.CompositeGraphicUnit, 0, defaults, nil, false, nil)
}
return bounds.box, nil
}
// measureImage 复用图片变换与裁剪语义度量范围,不读取像素数据
// 入参: ctx 度量画布, obj 图片, pageH 页面高度, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪
func (r *Renderer) measureImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
return
}
box, _ := ParseBox(obj.Boundary)
ctm := NewMatrix(obj.CTM)
if obj.CTM == "" {
ctm = Matrix{a: box.W, d: box.H}
}
m := TranslationMatrix(box.X, box.Y).Multiply(ctm)
if parentCTM != nil {
if boundaryInCTM {
m = parentCTM.Multiply(m)
} else {
m = TranslationMatrix(box.X, box.Y).Multiply(*parentCTM).Multiply(ctm)
}
}
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}})
clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, ctm, parentCTM, boundaryInCTM))
ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip))
if obj.Border != nil {
border := *obj.Border
border.BorderColor = (*StrokeColor)(boundsColor((*FillColor)(border.BorderColor)))
obj.Border = &border
r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clip)
}
}
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷
// 入参: color 填充或描边颜色
// 返回: *FillColor 用于度量的颜色
+4
View File
@@ -34,6 +34,10 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
if r.textOnly {
return
}
if _, ok := ctx.Renderer.(*boundsRenderer); ok {
r.measureImage(ctx, obj, pageH, parentCTM, boundaryInCTM, parentClip)
return
}
if obj.Visible != nil && !*obj.Visible {
return
}
+6
View File
@@ -304,6 +304,12 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
// renderPattern 渲染图案填充
// 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) {
if bounds, ok := ctx.Renderer.(*boundsRenderer); ok {
if pattern.alpha == nil || *pattern.alpha > 0 {
bounds.add(clip)
}
return
}
if r.pageText != nil {
renderer := *r
renderer.pageText = nil
+52 -10
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"io"
"maps"
"reflect"
"slices"
"strconv"
"time"
@@ -201,7 +202,7 @@ func (e *Editor) validate() error {
// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil
// 返回: error 错误信息
func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error {
fonts, images, _ := e.usedResources()
fonts, images, spaces, _ := e.usedResources()
writeXML := func(name string, encode func(*ofdXML)) error {
data, err := encodeOFDXML(encode)
if err != nil {
@@ -253,7 +254,7 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
x.start("PageArea", nil)
x.text("PhysicalBox", e.pages[0].Area.PhysicalBox)
x.end("PageArea")
if len(fonts)+len(images) != 0 {
if len(fonts)+len(images)+len(spaces) != 0 {
x.text("DocumentRes", "DocumentRes.xml")
}
x.end("CommonData")
@@ -270,8 +271,8 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
}); err != nil {
return err
}
if len(fonts)+len(images) != 0 {
if err := writeXML("Doc_0/DocumentRes.xml", func(x *ofdXML) { x.resources(fonts, images) }); err != nil {
if len(fonts)+len(images)+len(spaces) != 0 {
if err := writeXML("Doc_0/DocumentRes.xml", func(x *ofdXML) { x.resources(fonts, images, spaces) }); err != nil {
return err
}
}
@@ -299,10 +300,11 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
}
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池
// 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 原资源文件
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
used := make(map[string]bool)
promoted := make(map[string]bool)
files := make(map[string]bool)
for _, page := range e.pages {
var original map[string]GraphicObject
if e.source != nil {
@@ -318,6 +320,19 @@ func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []
for _, layer := range page.Content.Layer {
for _, object := range layer.Objects {
before, exists := original[editorObjectID(object)]
if origin := e.objectOrigin(editorObjectID(object)); origin != nil && !exists {
for _, name := range origin.page.original.PageRes {
files[e.source.reader.ResPath(resolveResourcePath(origin.page.ref.BaseLoc, "", name))] = true
}
}
for _, value := range []*FillColor{object.TextObject.FillColor, (*FillColor)(object.TextObject.StrokeColor), object.PathObject.FillColor, (*FillColor)(object.PathObject.StrokeColor)} {
if value != nil && value.ColorSpace != "" {
used[value.ColorSpace] = true
if e.source != nil && (!exists || !reflect.DeepEqual(before, object)) {
promoted[value.ColorSpace] = true
}
}
}
switch object.Type {
case "TextObject":
used[object.TextObject.Font] = true
@@ -348,14 +363,17 @@ func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []
images = append(images, resource)
delete(promoted, resource.image.ID)
}
if resource.space != nil && used[resource.space.ID] {
spaces = append(spaces, *resource.space)
delete(promoted, resource.space.ID)
}
}
files := make(map[string]bool)
for id := range promoted {
if name := e.source.reader.resourceFiles[id]; name != "" {
files[name] = true
}
}
return fonts, images, slices.Sorted(maps.Keys(files))
return fonts, images, spaces, slices.Sorted(maps.Keys(files))
}
// page 写出页面尺寸、图层和对象
@@ -387,9 +405,17 @@ func (x *ofdXML) layer(layer Layer) {
}
// resources 写出文档资源索引
// 入参: fonts 字体资源, images 图片资源
func (x *ofdXML) resources(fonts, images []editorResource) {
// 入参: fonts 字体资源, images 图片资源, spaces 颜色空间
func (x *ofdXML) resources(fonts, images []editorResource, spaces []ColorSpace) {
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "Res"}})
if len(spaces) != 0 {
x.start("ColorSpaces", nil)
for _, space := range spaces {
x.start("ColorSpace", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: space.ID}, {Name: xml.Name{Local: "Type"}, Value: space.Type}, {Name: xml.Name{Local: "BitsPerComponent"}, Value: strconv.Itoa(space.BitsPerComponent)}})
x.end("ColorSpace")
}
x.end("ColorSpaces")
}
if len(fonts) != 0 {
x.start("Fonts", nil)
for _, resource := range fonts {
@@ -616,10 +642,21 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.add("ImageMask", obj.ImageMask)
attrs.flag("Visible", obj.Visible)
attrs.alpha(obj.Alpha)
case "CompositeObject", "CompositeGraphicUnit":
obj := object.CompositeGraphicUnit
attrs.add("ID", obj.ID)
attrs.add("Boundary", obj.Boundary)
attrs.add("CTM", obj.CTM)
attrs.add("ResourceID", obj.ResourceID)
attrs.add("DrawParam", obj.DrawParam)
attrs.flag("Visible", obj.Visible)
attrs.alpha(obj.Alpha)
}
x.start(object.Type, attrs)
if object.Type == "ImageObject" {
x.clips(object.ImageObject.Clips)
} else if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
x.clips(object.CompositeGraphicUnit.Clips)
} else if object.Type == "PathObject" || object.Type == "Path" {
x.clips(object.PathObject.Clips)
x.color("StrokeColor", stroke)
@@ -655,6 +692,7 @@ func (x *ofdXML) clips(clips *Clips) {
for _, area := range clip.Area {
var attrs ofdAttrs
attrs.add("CTM", area.CTM)
attrs.add("DrawParam", area.DrawParam)
x.start("Area", attrs)
for _, path := range area.Path {
x.object(GraphicObject{Type: "Path", PathObject: path}, false)
@@ -690,6 +728,10 @@ func (x *ofdXML) color(name string, color *FillColor) {
}
var attrs ofdAttrs
attrs.add("Value", color.Value)
attrs.add("ColorSpace", color.ColorSpace)
if color.Index != nil {
attrs.add("Index", strconv.Itoa(*color.Index))
}
attrs.alpha(color.Alpha)
x.start(name, attrs)
x.end(name)