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