mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
19dc79e8e3
commit
a710bc5f41
68 files changed
+2063
-638
No files matched your search
@@ -2225,7 +2225,7 @@ function updateVersionControls() {
|
|||||||
el.versionList.inert = busy || Boolean(manager?.loading);
|
el.versionList.inert = busy || Boolean(manager?.loading);
|
||||||
el.versionName.disabled = el.versionAdd.disabled = unavailable || !state.editing;
|
el.versionName.disabled = el.versionAdd.disabled = unavailable || !state.editing;
|
||||||
el.versionRename.disabled = unavailable || !state.editing || !entry?.id || el.versionName.value.trim() === entry.name;
|
el.versionRename.disabled = unavailable || !state.editing || !entry?.id || el.versionName.value.trim() === entry.name;
|
||||||
el.versionDelete.disabled = unavailable || !state.editing || !entry?.id;
|
el.versionDelete.disabled = unavailable || !state.editing || !entry?.id || manager.entries.length < 2;
|
||||||
el.versionDefault.disabled = unavailable || !state.editing || Boolean(entry?.default);
|
el.versionDefault.disabled = unavailable || !state.editing || Boolean(entry?.default);
|
||||||
el.versionSelect.disabled = unavailable || entry?.id === (state.doc?.documentVersion || "");
|
el.versionSelect.disabled = unavailable || entry?.id === (state.doc?.documentVersion || "");
|
||||||
}
|
}
|
||||||
@@ -2695,7 +2695,7 @@ async function editCanvasObject(item) {
|
|||||||
}
|
}
|
||||||
if (item.type === "TextObject") {
|
if (item.type === "TextObject") {
|
||||||
if (!canEditObject(item, "textContent")) {
|
if (!canEditObject(item, "textContent")) {
|
||||||
if (!item.scoped && canEditObject(item, "rewriteText")) {
|
if (canEditObject(item, "rewriteText")) {
|
||||||
state.sourceText = { item, openSeq: state.openSeq };
|
state.sourceText = { item, openSeq: state.openSeq };
|
||||||
el.sourceTextValue.value = item.text;
|
el.sourceTextValue.value = item.text;
|
||||||
el.sourceTextStatus.textContent = "";
|
el.sourceTextStatus.textContent = "";
|
||||||
@@ -9081,7 +9081,7 @@ function updateObjectControls(item, reset = false) {
|
|||||||
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
el.resetCropButton.disabled = canEditObject(item, "resetCrop") ? disabled : el.cropImageButton.disabled || !cropping && (item.scoped ? !item.cropped : !item.imageBounds || ["x", "y", "width", "height"].every(key => Math.abs(item[key] - item.imageBounds[key]) < 1e-9));
|
||||||
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
el.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
|
||||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||||
|| !(item.type === "TextObject" && !item.scoped && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
|| !(item.type === "TextObject" && canEditObject(item, "rewriteText")) && !canEditObject(item, item.type === "TextObject" ? "textContent" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
||||||
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4
|
github.com/xiaoqidun/jbig2 v1.1.4
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb
|
github.com/xiaoqidun/pdfgo v0.0.0-20261012024528-667f6fbbcb3f
|
||||||
golang.org/x/crypto v0.58.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/text v0.43.0
|
golang.org/x/text v0.43.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e h1:OuRcvG5g5AieBN3
|
|||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb h1:oxXcuj9fGKNLDYmSSiJjQXKkxSWQpmaNJwoF4SEtPZU=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261012024528-667f6fbbcb3f h1:5fYd+Fv3MoSeS5cOEsSU7y4Oo2IXpDn14e1P/rZTvlY=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261012024528-667f6fbbcb3f/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+67
-34
@@ -1869,6 +1869,12 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
|
|||||||
if path.DashOffset != nil {
|
if path.DashOffset != nil {
|
||||||
item["dashOffset"] = *path.DashOffset
|
item["dashOffset"] = *path.DashOffset
|
||||||
}
|
}
|
||||||
|
if item["scoped"] == true {
|
||||||
|
item["dashPattern"] = editorDashPattern(path.DashPattern, scale)
|
||||||
|
if path.DashOffset != nil {
|
||||||
|
item["dashOffset"] = *path.DashOffset * scale
|
||||||
|
}
|
||||||
|
}
|
||||||
if paint {
|
if 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"], item["strokeColor"] = editorColorHex(path.FillColor), editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
|
item["fillColor"], item["strokeColor"] = editorColorHex(path.FillColor), editorColorHex((*ofdgo.FillColor)(path.StrokeColor))
|
||||||
@@ -2259,36 +2265,40 @@ func editorFont(args []js.Value) (any, error) {
|
|||||||
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorObjectAt 读取页面对象或复合对象成员的有效样式快照
|
||||||
|
// 入参: page 页面索引, key 界面对象路径
|
||||||
|
// 返回: ofdgo.GraphicObject 对象快照, error 错误信息
|
||||||
|
func editorObjectAt(page int, key string) (ofdgo.GraphicObject, error) {
|
||||||
|
if currentEditor == nil {
|
||||||
|
return ofdgo.GraphicObject{}, fmt.Errorf("no document is being edited")
|
||||||
|
}
|
||||||
|
if separator := strings.LastIndex(key, "/"); separator >= 0 {
|
||||||
|
path, err := compositePath(key[:separator])
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.GraphicObject{}, err
|
||||||
|
}
|
||||||
|
member, err := strconv.Atoi(key[separator+1:])
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.GraphicObject{}, err
|
||||||
|
}
|
||||||
|
members, err := currentEditor.CompositeObjectsAt(page, path, []int{member})
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.GraphicObject{}, err
|
||||||
|
}
|
||||||
|
return members[0].Object, nil
|
||||||
|
}
|
||||||
|
return currentEditor.Object(page, key)
|
||||||
|
}
|
||||||
|
|
||||||
// previewText 生成保留原定位的输入预览,不修改文档或历史
|
// previewText 生成保留原定位的输入预览,不修改文档或历史
|
||||||
// 入参: args 页码、对象路径、新文字、可选字体数据和字号
|
// 入参: args 页码、对象路径、新文字、可选字体数据和字号
|
||||||
// 返回: any 定位段及字体样式, error 错误信息
|
// 返回: any 定位段及字体样式, error 错误信息
|
||||||
func previewText(args []js.Value) (any, error) {
|
func previewText(args []js.Value) (any, error) {
|
||||||
if currentEditor == nil {
|
key := args[1].String()
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
object, err := editorObjectAt(args[0].Int(), key)
|
||||||
}
|
|
||||||
page, key := args[0].Int(), args[1].String()
|
|
||||||
var object ofdgo.GraphicObject
|
|
||||||
if separator := strings.LastIndex(key, "/"); separator >= 0 {
|
|
||||||
path, err := compositePath(key[:separator])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
member, err := strconv.Atoi(key[separator+1:])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
members, err := currentEditor.CompositeObjectsAt(page, path, []int{member})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
object = members[0].Object
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
object, err = currentEditor.Object(page, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text := object.TextObject
|
text := object.TextObject
|
||||||
if object.Type != "TextObject" {
|
if object.Type != "TextObject" {
|
||||||
return nil, fmt.Errorf("object is not text")
|
return nil, fmt.Errorf("object is not text")
|
||||||
@@ -2361,10 +2371,10 @@ func previewText(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
|
||||||
// 入参: args 页面、对象标识和内容
|
// 入参: args 页面、对象路径和内容
|
||||||
// 返回: any SVG预览, error 错误信息
|
// 返回: any SVG预览, error 错误信息
|
||||||
func previewPositionedText(args []js.Value) (any, error) {
|
func previewPositionedText(args []js.Value) (any, error) {
|
||||||
object, err := currentEditor.Object(args[0].Int(), args[1].String())
|
object, err := editorObjectAt(args[0].Int(), args[1].String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -3190,6 +3200,24 @@ func resolveStylePaint(paint *ofdgo.FillColor) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorDashPattern 将内部对象的虚线长度格式化为页面毫米
|
||||||
|
// 入参: value 对象空间虚线长度, scale 页面描边倍率
|
||||||
|
// 返回: string 页面空间虚线长度,无法解析时保留原文
|
||||||
|
func editorDashPattern(value string, scale float64) string {
|
||||||
|
if scale == 1 || value == "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
fields := strings.Fields(value)
|
||||||
|
for i, field := range fields {
|
||||||
|
number, err := strconv.ParseFloat(field, 64)
|
||||||
|
if err != nil || math.IsNaN(number*scale) || math.IsInf(number*scale, 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
fields[i] = strconv.FormatFloat(number*scale, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
return strings.Join(fields, " ")
|
||||||
|
}
|
||||||
|
|
||||||
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
|
// objectStyle 读取高级面板提交的外观字段,省略字段保持原值
|
||||||
// 入参: value 前端样式
|
// 入参: value 前端样式
|
||||||
// 返回: ofdgo.ObjectStyle 库层样式
|
// 返回: ofdgo.ObjectStyle 库层样式
|
||||||
@@ -3453,21 +3481,26 @@ func documentVersions(args []js.Value) (any, error) {
|
|||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
var versions []ofdgo.DocumentVersionInfo
|
reader := currentSession.Reader
|
||||||
var err error
|
|
||||||
if currentEditor != nil {
|
if currentEditor != nil {
|
||||||
versions, err = currentEditor.DocumentVersions()
|
var err error
|
||||||
} else {
|
reader, err = currentEditor.Reader()
|
||||||
versions, err = currentSession.Reader.DocumentVersions()
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries := make([]VersionEntry, 1, len(versions)+1)
|
}
|
||||||
entries[0].Default = true
|
versions, err := reader.DocumentVersions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries := make([]VersionEntry, 0, len(versions)+1)
|
||||||
|
hasMain := reader.OFD.DocBody[reader.DocumentIndex()].DocRoot != ""
|
||||||
|
if hasMain {
|
||||||
|
entries = append(entries, VersionEntry{Default: true})
|
||||||
|
}
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
entries = append(entries, VersionEntry{ID: version.ID, Index: version.Index, Name: version.Name, Default: version.Current})
|
entries = append(entries, VersionEntry{ID: version.ID, Index: version.Index, Name: version.Name, Default: version.Current})
|
||||||
if version.Current {
|
if version.Current && hasMain {
|
||||||
entries[0].Default = false
|
entries[0].Default = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-8
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
@@ -149,13 +150,13 @@ func (r *Reader) annotationInfos(index int, pageID string) []AnnotationInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseAnnotations 解析注释文件
|
// parseAnnotations 解析注释文件
|
||||||
// 入参: doc 文档结构
|
// 入参: ctx 取消上下文, doc 文档结构
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Reader) parseAnnotations(doc *Document) error {
|
func (r *Reader) parseAnnotations(ctx context.Context, doc *Document) error {
|
||||||
if doc.Annotations == "" {
|
if doc.Annotations == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
annotations, err := r.annotationIndex(doc)
|
annotations, err := r.annotationIndexContext(ctx, doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -164,26 +165,39 @@ func (r *Reader) parseAnnotations(doc *Document) error {
|
|||||||
}
|
}
|
||||||
annPath := r.ResPath(doc.Annotations)
|
annPath := r.ResPath(doc.Annotations)
|
||||||
for _, page := range annotations.Page {
|
for _, page := range annotations.Page {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
annotPath := resolveResourcePath(annPath, "", page.FileLoc)
|
annotPath := resolveResourcePath(annPath, "", page.FileLoc)
|
||||||
af, err := r.openFile(annotPath)
|
af, err := r.openFile(annotPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var pageAnnot PageAnnot
|
var pageAnnot PageAnnot
|
||||||
err = xml.NewDecoder(af).Decode(&pageAnnot)
|
err = xml.NewDecoder(imageInput{ReadCloser: af, context: ctx}).Decode(&pageAnnot)
|
||||||
_ = af.Close()
|
_ = af.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r.Annots[page.PageID] = append(r.Annots[page.PageID], pageAnnot.Annot...)
|
r.Annots[page.PageID] = append(r.Annots[page.PageID], pageAnnot.Annot...)
|
||||||
}
|
}
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// annotationIndex 读取注解索引,将页引用对应到文档声明的标识,不解析页面外观
|
// annotationIndex 读取注解索引,将页引用对应到文档声明的标识,不解析页面外观
|
||||||
// 入参: doc 文档结构
|
// 入参: doc 文档结构
|
||||||
// 返回: Annotations 注解索引, error 错误信息
|
// 返回: Annotations 注解索引, error 错误信息
|
||||||
func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
|
func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
|
||||||
|
return r.annotationIndexContext(context.Background(), doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// annotationIndexContext 读取注解索引并规范页引用,取消时不提交文件索引
|
||||||
|
// 入参: ctx 取消上下文, doc 文档结构
|
||||||
|
// 返回: Annotations 注解索引, error 读取或取消错误
|
||||||
|
func (r *Reader) annotationIndexContext(ctx context.Context, doc *Document) (Annotations, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Annotations{}, err
|
||||||
|
}
|
||||||
if doc.Annotations == "" {
|
if doc.Annotations == "" {
|
||||||
return Annotations{}, nil
|
return Annotations{}, nil
|
||||||
}
|
}
|
||||||
@@ -194,17 +208,23 @@ func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
var annotations Annotations
|
var annotations Annotations
|
||||||
if err := xml.NewDecoder(f).Decode(&annotations); err != nil {
|
if err := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx}).Decode(&annotations); err != nil {
|
||||||
return Annotations{}, err
|
return Annotations{}, err
|
||||||
}
|
}
|
||||||
pageIDs := make(map[string]string, len(doc.Pages.Page))
|
pageIDs := make(map[string]string, len(doc.Pages.Page))
|
||||||
for _, page := range doc.Pages.Page {
|
for _, page := range doc.Pages.Page {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Annotations{}, err
|
||||||
|
}
|
||||||
if id := editorResourceID(page.ID); id != "" {
|
if id := editorResourceID(page.ID); id != "" {
|
||||||
pageIDs[id] = page.ID
|
pageIDs[id] = page.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.annotationFiles = make(map[string][]string)
|
files := make(map[string][]string)
|
||||||
for i := range annotations.Page {
|
for i := range annotations.Page {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Annotations{}, err
|
||||||
|
}
|
||||||
page := &annotations.Page[i]
|
page := &annotations.Page[i]
|
||||||
if id := pageIDs[editorResourceID(page.PageID)]; id != "" {
|
if id := pageIDs[editorResourceID(page.PageID)]; id != "" {
|
||||||
page.PageID = id
|
page.PageID = id
|
||||||
@@ -213,7 +233,11 @@ func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
|
|||||||
if file, ok := r.packageFile(annotPath); ok {
|
if file, ok := r.packageFile(annotPath); ok {
|
||||||
annotPath = cleanPackagePath(file.Name)
|
annotPath = cleanPackagePath(file.Name)
|
||||||
}
|
}
|
||||||
r.annotationFiles[annotPath] = append(r.annotationFiles[annotPath], page.PageID)
|
files[annotPath] = append(files[annotPath], page.PageID)
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return Annotations{}, err
|
||||||
|
}
|
||||||
|
r.annotationFiles = files
|
||||||
return annotations, nil
|
return annotations, nil
|
||||||
}
|
}
|
||||||
+9
-18
@@ -140,7 +140,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
return &snapshot, nil
|
return &snapshot, nil
|
||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
protected, err := e.source.reader.hasOutputSignatures()
|
protected, err := e.source.reader.hasOutputSignatures(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -285,28 +285,19 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
return paths, safe, ctx.Err()
|
return paths, safe, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasOutputSignatures 检查包中各文档,避免改写其他文档签名保护的共享资源
|
// hasOutputSignatures 检查全部文档的签名入口,不受当前文档或版本选择影响
|
||||||
// 返回: bool 是否存在签名, error 文档读取错误
|
// 入参: ctx 取消上下文
|
||||||
func (r *Reader) hasOutputSignatures() (bool, error) {
|
// 返回: bool 是否声明签名入口, error 取消错误
|
||||||
|
func (r *Reader) hasOutputSignatures(ctx context.Context) (bool, error) {
|
||||||
for _, body := range r.OFD.DocBody {
|
for _, body := range r.OFD.DocBody {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
if body.Signatures != "" {
|
if body.Signatures != "" {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
data, err := r.readFile(body.DocRoot)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
var doc struct {
|
return false, ctx.Err()
|
||||||
Signatures string `xml:"Signatures"`
|
|
||||||
}
|
|
||||||
if err := xml.Unmarshal(data, &doc); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if doc.Signatures != "" {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeOutputEntry 优化ZIP编码,保留资源名称及条目的未压缩字节
|
// writeOutputEntry 优化ZIP编码,保留资源名称及条目的未压缩字节
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
|
|||||||
document := r
|
document := r
|
||||||
if index != r.DocumentIndex() {
|
if index != r.DocumentIndex() {
|
||||||
var err error
|
var err error
|
||||||
document, err = r.Document(index)
|
document, err = r.DocumentContext(ctx, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -112,18 +112,19 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
|
|||||||
if dpi == 0 {
|
if dpi == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
count, err := r.PageCount()
|
doc, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
|
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
|
||||||
|
renderer.pageContext = ctx
|
||||||
defer renderer.ClearCache()
|
defer renderer.ClearCache()
|
||||||
visitor := &compressionImages{ctx: ctx, dpi: dpi, sizes: make(map[string]image.Point), renderer: renderer}
|
visitor := &compressionImages{ctx: ctx, dpi: dpi, sizes: make(map[string]image.Point), renderer: renderer}
|
||||||
for index := 0; index < count; index++ {
|
for _, ref := range doc.Pages.Page {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
page, err := r.PageContentByIndex(index)
|
page, err := r.PageContentContext(ctx, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,8 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
@@ -40,37 +37,38 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
|
|||||||
seen := make(map[[32]byte]string)
|
seen := make(map[[32]byte]string)
|
||||||
aliases := make(map[string]string)
|
aliases := make(map[string]string)
|
||||||
indexes := make(map[string]map[string]string)
|
indexes := make(map[string]map[string]string)
|
||||||
buffer := make([]byte, 32*1024)
|
ordered := slices.Sorted(maps.Keys(profiles))
|
||||||
for _, name := range slices.Sorted(maps.Keys(profiles)) {
|
sizes := make(map[string]uint64, len(profiles))
|
||||||
|
counts := make(map[uint64]int)
|
||||||
|
for _, name := range ordered {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if protected[name] {
|
if protected[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var input io.ReadCloser
|
size, err := outputResourceSize(reader, parts, name)
|
||||||
if data, ok := parts[name]; ok {
|
|
||||||
input = io.NopCloser(bytes.NewReader(data))
|
|
||||||
} else {
|
|
||||||
if reader == nil {
|
|
||||||
return fmt.Errorf("file not found: %s", name)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
input, err = reader.openFile(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
sizes[name] = size
|
||||||
|
counts[size]++
|
||||||
}
|
}
|
||||||
hash := sha256.New()
|
var buffer []byte
|
||||||
_, err := io.CopyBuffer(hash, imageInput{ReadCloser: input, context: ctx}, buffer)
|
for _, name := range ordered {
|
||||||
closeErr := input.Close()
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if protected[name] || counts[sizes[name]] < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if buffer == nil {
|
||||||
|
buffer = make([]byte, 32*1024)
|
||||||
|
}
|
||||||
|
key, err := outputResourceDigest(ctx, reader, parts, name, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if closeErr != nil {
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
key := [32]byte(hash.Sum(nil))
|
|
||||||
first, found := seen[key]
|
first, found := seen[key]
|
||||||
if !found {
|
if !found {
|
||||||
seen[key] = name
|
seen[key] = name
|
||||||
|
|||||||
+103
-16
@@ -16,10 +16,12 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -105,6 +107,10 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
type resourceFile struct{ key, role string }
|
type resourceFile struct{ key, role string }
|
||||||
|
type resourceSize struct {
|
||||||
|
role string
|
||||||
|
size uint64
|
||||||
|
}
|
||||||
files := make(map[resourceFile]bool)
|
files := make(map[resourceFile]bool)
|
||||||
for _, name := range resources {
|
for _, name := range resources {
|
||||||
data, err := read(name)
|
data, err := read(name)
|
||||||
@@ -171,31 +177,43 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
imageResults := make(map[outputImageKey][]byte)
|
imageResults := make(map[outputImageKey][]byte)
|
||||||
imageSources := make(map[string]outputImageSource)
|
imageSources := make(map[string]outputImageSource)
|
||||||
ordered := slices.Sorted(maps.Keys(actual))
|
ordered := slices.Sorted(maps.Keys(actual))
|
||||||
|
sizes := make(map[string]uint64, len(files))
|
||||||
|
largeCounts := make(map[resourceSize]int)
|
||||||
var imageKeys []string
|
var imageKeys []string
|
||||||
for _, key := range ordered {
|
for _, key := range ordered {
|
||||||
if !refs.files[key] && files[resourceFile{key, "MediaFile"}] && !files[resourceFile{key, "FontFile"}] {
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
font, media := files[resourceFile{key, "FontFile"}], files[resourceFile{key, "MediaFile"}]
|
||||||
|
if refs.files[key] || font == media {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size, err := outputResourceSize(reader, parts, actual[key])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sizes[key] = size
|
||||||
|
if size > outputOptimizationBufferLimit {
|
||||||
|
role := "MediaFile"
|
||||||
|
if font {
|
||||||
|
role = "FontFile"
|
||||||
|
}
|
||||||
|
largeCounts[resourceSize{role, size}]++
|
||||||
|
} else if media {
|
||||||
imageKeys = append(imageKeys, key)
|
imageKeys = append(imageKeys, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
load := func(key string) ([]byte, error) {
|
load := func(key string) ([]byte, error) {
|
||||||
if data, ok := parts[actual[key]]; ok && len(data) > outputOptimizationBufferLimit {
|
if sizes[key] > outputOptimizationBufferLimit {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if reader != nil && parts[actual[key]] == nil {
|
return read(actual[key])
|
||||||
if file, ok := reader.packageFile(actual[key]); ok && file.UncompressedSize64 > outputOptimizationBufferLimit {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data, err := read(actual[key])
|
|
||||||
if len(data) > outputOptimizationBufferLimit {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
}
|
||||||
if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil {
|
if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var fontCache editorSourceFontCache
|
var fontCache editorSourceFontCache
|
||||||
|
var hashBuffer []byte
|
||||||
for _, key := range ordered {
|
for _, key := range ordered {
|
||||||
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
|
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
|
||||||
continue
|
continue
|
||||||
@@ -207,20 +225,36 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
if err := editorProgress(e.OnWriteProgress).withContext(e.output.ctx).report("compress", e.output.completed, 0); err != nil {
|
if err := editorProgress(e.OnWriteProgress).withContext(e.output.ctx).report("compress", e.output.completed, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if size := sizes[key]; size > outputOptimizationBufferLimit && largeCounts[resourceSize{role, size}] < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
source, prefetched := imageSources[key]
|
source, prefetched := imageSources[key]
|
||||||
|
usage := fontUsage[key]
|
||||||
|
subset := role == "FontFile" && usage != nil && !usage.unsafe && e.backends.FontResources != nil
|
||||||
var data []byte
|
var data []byte
|
||||||
|
optimizable := prefetched
|
||||||
if !prefetched {
|
if !prefetched {
|
||||||
var err error
|
var err error
|
||||||
|
if role == "MediaFile" || subset {
|
||||||
data, err = load(key)
|
data, err = load(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if data == nil {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
optimizable = data != nil
|
||||||
|
if optimizable {
|
||||||
source.key.hash = sha256.Sum256(data)
|
source.key.hash = sha256.Sum256(data)
|
||||||
|
} else {
|
||||||
|
if hashBuffer == nil {
|
||||||
|
hashBuffer = make([]byte, 32*1024)
|
||||||
}
|
}
|
||||||
if usage := fontUsage[key]; role == "FontFile" && usage != nil && !usage.unsafe {
|
source.key.hash, err = outputResourceDigest(e.output.ctx, reader, parts, actual[key], hashBuffer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if optimizable && subset {
|
||||||
candidate, err := fontCache.subset(e.output.ctx, e.backends.FontResources, data, usage)
|
candidate, err := fontCache.subset(e.output.ctx, e.backends.FontResources, data, usage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("subset font %s: %w", key, err)
|
return fmt.Errorf("subset font %s: %w", key, err)
|
||||||
@@ -234,7 +268,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
source.key.hash = sha256.Sum256(data)
|
source.key.hash = sha256.Sum256(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if role == "MediaFile" {
|
if role == "MediaFile" && optimizable {
|
||||||
options, demand := e.outputImageOptions(key)
|
options, demand := e.outputImageOptions(key)
|
||||||
cacheKey := outputImageKey{hash: source.key.hash, options: options, size: demand}
|
cacheKey := outputImageKey{hash: source.key.hash, options: options, size: demand}
|
||||||
candidate, cached := imageResults[cacheKey]
|
candidate, cached := imageResults[cacheKey]
|
||||||
@@ -304,6 +338,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(aliases) == 0 && len(formats) == 0 {
|
||||||
|
return e.output.ctx.Err()
|
||||||
|
}
|
||||||
for _, name := range resources {
|
for _, name := range resources {
|
||||||
if refs.versions[cleanPackagePath(name)] {
|
if refs.versions[cleanPackagePath(name)] {
|
||||||
continue
|
continue
|
||||||
@@ -357,3 +394,53 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
}
|
}
|
||||||
return e.output.ctx.Err()
|
return e.output.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputResourceSize 获取资源的实际输出大小,优先使用本次改动及已加载数据
|
||||||
|
// 入参: reader 原包,可为空, parts 输出改动, name 资源路径
|
||||||
|
// 返回: uint64 未压缩字节数, error 文件不存在
|
||||||
|
func outputResourceSize(reader *Reader, parts map[string][]byte, name string) (uint64, error) {
|
||||||
|
if data, ok := parts[name]; ok {
|
||||||
|
return uint64(len(data)), nil
|
||||||
|
}
|
||||||
|
if reader != nil {
|
||||||
|
if data, ok := reader.files[cleanPackagePath(name)]; ok {
|
||||||
|
return uint64(len(data)), nil
|
||||||
|
}
|
||||||
|
if file, ok := reader.packageFile(name); ok {
|
||||||
|
return file.UncompressedSize64, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("file not found: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// outputResourceDigest 流式计算资源摘要,不将原包中的大文件加载到内存
|
||||||
|
// 入参: ctx 取消上下文, reader 原包,可为空, parts 输出改动, name 资源路径, buffer 复用缓冲
|
||||||
|
// 返回: [32]byte 内容摘要, error 读取或取消错误
|
||||||
|
func outputResourceDigest(ctx context.Context, reader *Reader, parts map[string][]byte, name string, buffer []byte) ([32]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
var input io.ReadCloser
|
||||||
|
if data, ok := parts[name]; ok {
|
||||||
|
input = io.NopCloser(bytes.NewReader(data))
|
||||||
|
} else {
|
||||||
|
if reader == nil {
|
||||||
|
return [32]byte{}, fmt.Errorf("file not found: %s", name)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
input, err = reader.openFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hash := sha256.New()
|
||||||
|
_, err := io.CopyBuffer(hash, imageInput{ReadCloser: input, context: ctx}, buffer)
|
||||||
|
closeErr := input.Close()
|
||||||
|
if err != nil {
|
||||||
|
return [32]byte{}, err
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return [32]byte{}, closeErr
|
||||||
|
}
|
||||||
|
return [32]byte(hash.Sum(nil)), ctx.Err()
|
||||||
|
}
|
||||||
+2
-1
@@ -21,6 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Document 文档结构
|
// Document 文档结构
|
||||||
|
// Signatures由阅读器按主入口和所选版本解析,不属于文档根节点的XML内容
|
||||||
type Document struct {
|
type Document struct {
|
||||||
XMLName xml.Name `xml:"Document"`
|
XMLName xml.Name `xml:"Document"`
|
||||||
CommonData CommonData `xml:"CommonData"`
|
CommonData CommonData `xml:"CommonData"`
|
||||||
@@ -30,7 +31,7 @@ type Document struct {
|
|||||||
Actions []Action `xml:"Actions>Action"`
|
Actions []Action `xml:"Actions>Action"`
|
||||||
Bookmarks Bookmarks `xml:"Bookmarks"`
|
Bookmarks Bookmarks `xml:"Bookmarks"`
|
||||||
Annotations string `xml:"Annotations"`
|
Annotations string `xml:"Annotations"`
|
||||||
Signatures string `xml:"Signatures"`
|
Signatures string `xml:"-"`
|
||||||
Attachments Attachments `xml:"Attachments"`
|
Attachments Attachments `xml:"Attachments"`
|
||||||
CustomTags CustomTags `xml:"CustomTags"`
|
CustomTags CustomTags `xml:"CustomTags"`
|
||||||
Extensions Extensions `xml:"Extensions"`
|
Extensions Extensions `xml:"Extensions"`
|
||||||
|
|||||||
+2
-1
@@ -78,8 +78,9 @@ func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
|
|||||||
if dp.DashOffset != nil {
|
if dp.DashOffset != nil {
|
||||||
base.DashOffset = dp.DashOffset
|
base.DashOffset = dp.DashOffset
|
||||||
}
|
}
|
||||||
if dp.MiterLimit > 0 {
|
if dp.MiterLimit > 0 || dp.MiterLimitSet {
|
||||||
base.MiterLimit = dp.MiterLimit
|
base.MiterLimit = dp.MiterLimit
|
||||||
|
base.MiterLimitSet = dp.MiterLimitSet
|
||||||
}
|
}
|
||||||
if dp.FillColor != nil {
|
if dp.FillColor != nil {
|
||||||
base.FillColor = dp.FillColor
|
base.FillColor = dp.FillColor
|
||||||
|
|||||||
+26
-4
@@ -1045,6 +1045,11 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
|
|||||||
obj.LineWidth = defaultPathLineWidth
|
obj.LineWidth = defaultPathLineWidth
|
||||||
}
|
}
|
||||||
obj.LineWidth *= scale
|
obj.LineWidth *= scale
|
||||||
|
obj.DashPattern = scaleTextNumbers(obj.DashPattern, scale)
|
||||||
|
if obj.DashOffset != nil {
|
||||||
|
offset := *obj.DashOffset * scale
|
||||||
|
obj.DashOffset = &offset
|
||||||
|
}
|
||||||
for i := range obj.TextCode {
|
for i := range obj.TextCode {
|
||||||
code := &obj.TextCode[i]
|
code := &obj.TextCode[i]
|
||||||
for _, value := range []*string{&code.X, &code.Y, &code.DeltaX, &code.DeltaY} {
|
for _, value := range []*string{&code.X, &code.Y, &code.DeltaX, &code.DeltaY} {
|
||||||
@@ -1099,6 +1104,9 @@ func scaleTextNumbers(value string, scale float64) string {
|
|||||||
// 入参: obj 文字对象
|
// 入参: obj 文字对象
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) prepareText(obj *TextObject) error {
|
func (e *Editor) prepareText(obj *TextObject) error {
|
||||||
|
if err := validateEditorStroke(obj.pathStyle()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if id := editorResourceID(obj.Font); id != "" {
|
if id := editorResourceID(obj.Font); id != "" {
|
||||||
obj.Font = id
|
obj.Font = id
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1141,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
|
|||||||
if err := validateTextGlyphs(*obj, glyphCount); err != nil {
|
if err := validateTextGlyphs(*obj, glyphCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !finite(obj.HScale) || obj.HScale < 0 || !finite(obj.LineWidth) || obj.LineWidth < 0 || !finite(obj.MiterLimit) || obj.MiterLimit < 0 {
|
if !finite(obj.HScale) || obj.HScale < 0 {
|
||||||
return fmt.Errorf("invalid text dimensions")
|
return fmt.Errorf("invalid text dimensions")
|
||||||
}
|
}
|
||||||
if obj.Weight < 0 || obj.Weight > 900 || obj.Weight%100 != 0 {
|
if obj.Weight < 0 || obj.Weight > 900 || obj.Weight%100 != 0 {
|
||||||
@@ -1294,10 +1302,24 @@ func creationBox(value string) (Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Box{}, err
|
return Box{}, err
|
||||||
}
|
}
|
||||||
if values[2] <= 0 || values[3] <= 0 {
|
box := Box{X: values[0], Y: values[1], W: values[2], H: values[3]}
|
||||||
return Box{}, fmt.Errorf("boundary dimensions must be positive")
|
if err := validateCreationBox(box); err != nil {
|
||||||
|
return Box{}, err
|
||||||
}
|
}
|
||||||
return Box{X: values[0], Y: values[1], W: values[2], H: values[3]}, nil
|
return box, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCreationBox 校验编辑范围的有限坐标、端点及正尺寸
|
||||||
|
// 入参: box 矩形范围
|
||||||
|
// 返回: error 非有限坐标或非正尺寸
|
||||||
|
func validateCreationBox(box Box) error {
|
||||||
|
if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || !finite(box.X+box.W) || !finite(box.Y+box.H) {
|
||||||
|
return fmt.Errorf("boundary coordinates must be finite")
|
||||||
|
}
|
||||||
|
if box.W <= 0 || box.H <= 0 {
|
||||||
|
return fmt.Errorf("boundary dimensions must be positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// creationColor 校验默认RGB颜色空间中的基本颜色
|
// creationColor 校验默认RGB颜色空间中的基本颜色
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func (e *Editor) FlipCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, box 页面毫米目标范围
|
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, box 页面毫米目标范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
@@ -219,30 +219,46 @@ func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleCompositeObjects 原子修改成员透明度、文字颜色及路径填充、描边和线宽
|
// StyleCompositeObjects 原子修改成员透明度及文字、路径的填充和描边
|
||||||
// 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
|
// 不重排文字,不改写继承参数;显式实线覆盖原虚线继承
|
||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽使用页面毫米
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, style 待修改属性,线宽、虚线长度及偏移使用页面毫米
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
|
func (e *Editor) StyleCompositeObjects(page int, path ObjectPath, indexes []int, style ObjectStyle) error {
|
||||||
|
if style.DashPattern != nil {
|
||||||
|
if err := validateEditorStroke(PathObject{DashPattern: *style.DashPattern}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
paint := style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.MiterLimit != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
capability := members[i].Capabilities
|
capability := members[i].Capabilities
|
||||||
if !capability.Transform || paint && !capability.Paint {
|
if !capability.Transform || paint && !capability.Paint {
|
||||||
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
|
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
|
||||||
}
|
}
|
||||||
local := style
|
local := style
|
||||||
|
var painted PathObject
|
||||||
switch members[i].Object.Type {
|
switch members[i].Object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
|
painted = members[i].Object.TextObject.pathStyle()
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
p := members[i].Object.PathObject
|
painted = members[i].Object.PathObject
|
||||||
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
|
}
|
||||||
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
|
if members[i].Object.Type == "TextObject" || members[i].Object.Type == "PathObject" {
|
||||||
|
local.FillColor = editorStyleColor(style.FillColor, painted.FillColor)
|
||||||
|
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(painted.StrokeColor)))
|
||||||
if style.LineWidth != nil {
|
if style.LineWidth != nil {
|
||||||
width := *style.LineWidth / members[i].StrokeScale
|
width := *style.LineWidth / members[i].StrokeScale
|
||||||
local.LineWidth = &width
|
local.LineWidth = &width
|
||||||
}
|
}
|
||||||
|
if style.DashPattern != nil {
|
||||||
|
dash := scaleTextNumbers(*style.DashPattern, 1/members[i].StrokeScale)
|
||||||
|
local.DashPattern = &dash
|
||||||
|
}
|
||||||
|
if style.DashOffset != nil {
|
||||||
|
offset := *style.DashOffset / members[i].StrokeScale
|
||||||
|
local.DashOffset = &offset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
object, err := e.styleObject(node.object, local)
|
object, err := e.styleObject(node.object, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -650,6 +666,13 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if transform && err == nil && node.object.Type == "TextObject" {
|
||||||
|
capability.RewriteText = capability.TextContent
|
||||||
|
if !capability.RewriteText {
|
||||||
|
text := style.TextObject
|
||||||
|
capability.RewriteText = e.RewriteText(&text, text.Text()) == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if transform && node.object.Type == "ImageObject" && bounds.Bounds.W > 0 && bounds.Bounds.H > 0 {
|
if transform && node.object.Type == "ImageObject" && bounds.Bounds.W > 0 && bounds.Bounds.H > 0 {
|
||||||
clips := node.object.ImageObject.Clips
|
clips := node.object.ImageObject.Clips
|
||||||
_, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert()
|
_, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
// 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围
|
// 入参: page 页面索引, path 父复合路径, index 图片序号, box 页面毫米范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Box) error {
|
func (e *Editor) CropCompositeImage(page int, path ObjectPath, index int, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ func (e *Editor) wrapCompositePath(renderer *Renderer, node *editorCompositeNode
|
|||||||
// 入参: node 页面坐标中的对象节点
|
// 入参: node 页面坐标中的对象节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
||||||
|
if err := e.isolateCompositeDefaultPaint(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" || node.object.Type == "PathObject" {
|
if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" || node.object.Type == "PathObject" {
|
||||||
id, err := e.neutralDrawParam()
|
id, err := e.neutralDrawParam()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,7 +223,149 @@ func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
|
|||||||
return node.update(object)
|
return node.update(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// neutralDrawParam 注册可复用的完整默认绘制参数,不改变文档默认颜色空间
|
// isolateCompositeDefaultPaint 固定基准参数会覆盖的默认透明色,按继承环境复用资源副本
|
||||||
|
// 入参: node 待复制的独立节点
|
||||||
|
// 返回: error 参数、资源或XML错误
|
||||||
|
func (e *Editor) isolateCompositeDefaultPaint(node *editorCompositeNode) error {
|
||||||
|
type resourceKey struct {
|
||||||
|
id string
|
||||||
|
fill, stroke bool
|
||||||
|
}
|
||||||
|
cache := make(map[resourceKey]string)
|
||||||
|
visiting := make(map[string]bool)
|
||||||
|
var transparent *FillColor
|
||||||
|
var isolate func(*editorCompositeNode, DrawParam) error
|
||||||
|
isolate = func(current *editorCompositeNode, base DrawParam) error {
|
||||||
|
parameter, err := e.editorDrawParam(current.node.attr("DrawParam"), make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
base = *mergeDrawParam(base, parameter)
|
||||||
|
if base.FillColor != nil && base.StrokeColor != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
object := current.object
|
||||||
|
path := object.Type == "PathObject" && object.PathObject.FillColor == nil && base.FillColor == nil
|
||||||
|
text := object.Type == "TextObject" && object.TextObject.StrokeColor == nil && base.StrokeColor == nil
|
||||||
|
if path || text {
|
||||||
|
if transparent == nil {
|
||||||
|
transparent, err = e.RGBColor(color.NRGBA{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path {
|
||||||
|
object.PathObject.FillColor = transparent
|
||||||
|
} else {
|
||||||
|
object.TextObject.StrokeColor = (*StrokeColor)(transparent)
|
||||||
|
}
|
||||||
|
return current.update(object)
|
||||||
|
}
|
||||||
|
if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
id := object.CompositeGraphicUnit.ResourceID
|
||||||
|
replacement := id
|
||||||
|
if id != "" {
|
||||||
|
key := resourceKey{editorResourceID(id), base.FillColor != nil, base.StrokeColor != nil}
|
||||||
|
if key.id == "" {
|
||||||
|
key.id = id
|
||||||
|
}
|
||||||
|
if visiting[key.id] {
|
||||||
|
return fmt.Errorf("cyclic composite resource %q", id)
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
replacement, ok = cache[key]
|
||||||
|
if !ok {
|
||||||
|
resource, err := e.compositeDefinition(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
visiting[key.id] = true
|
||||||
|
err = isolate(resource, base)
|
||||||
|
delete(visiting, key.id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
replacement = id
|
||||||
|
if resource.changed {
|
||||||
|
for _, entry := range e.resources {
|
||||||
|
if sameResourceID(entry.composite, id) {
|
||||||
|
resource.setStates(entry.states)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replacement, err = e.addCompositeResource(resource.data, resource.states)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache[key] = replacement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
var visit func(*editorXML) error
|
||||||
|
visit = func(parent *editorXML) error {
|
||||||
|
for _, child := range parent.children {
|
||||||
|
if classifyOFDNamespace(child.name.Space) == ofdXMLUnknown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch child.name.Local {
|
||||||
|
case "Content", "PageBlock":
|
||||||
|
if err := visit(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit":
|
||||||
|
if child.name.Local == "TextObject" && (base.StrokeColor != nil || child.child("StrokeColor") != nil) || child.name.Local == "PathObject" && (base.FillColor != nil || child.child("FillColor") != nil) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := editorXMLStandalone(current.data[child.start:child.end], child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
member, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := isolate(member, base); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if member.changed {
|
||||||
|
patches = append(patches, editorXMLPatch{child.start, child.end, member.data})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := visit(current.node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(patches) != 0 {
|
||||||
|
next, err := newEditorCompositeNode(editorPatchXML(current.data, patches))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
current.data, current.node, current.object, current.changed = next.data, next.node, next.object, true
|
||||||
|
}
|
||||||
|
if replacement != id {
|
||||||
|
object = current.object
|
||||||
|
object.CompositeGraphicUnit.ResourceID = replacement
|
||||||
|
return current.update(object)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var base DrawParam
|
||||||
|
for _, id := range node.drawParams {
|
||||||
|
parameter, err := e.editorDrawParam(id, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
base = *mergeDrawParam(base, parameter)
|
||||||
|
}
|
||||||
|
return isolate(node, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// neutralDrawParam 注册隔离继承用的基准参数,默认透明色由对象显式保留
|
||||||
// 返回: string 绘制参数标识, error 错误信息
|
// 返回: string 绘制参数标识, error 错误信息
|
||||||
func (e *Editor) neutralDrawParam() (string, error) {
|
func (e *Editor) neutralDrawParam() (string, error) {
|
||||||
fill, err := e.RGBColor(color.NRGBA{A: 255})
|
fill, err := e.RGBColor(color.NRGBA{A: 255})
|
||||||
@@ -261,6 +406,9 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
|||||||
attrs.add("LineWidth", "0")
|
attrs.add("LineWidth", "0")
|
||||||
}
|
}
|
||||||
attrs.number("MiterLimit", draw.MiterLimit)
|
attrs.number("MiterLimit", draw.MiterLimit)
|
||||||
|
if draw.MiterLimitSet && draw.MiterLimit == 0 {
|
||||||
|
attrs.add("MiterLimit", "0")
|
||||||
|
}
|
||||||
if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" {
|
if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" {
|
||||||
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func (e *Editor) reshapeCompositeObject(page int, path ObjectPath, index int, ki
|
|||||||
if lineShapeKind(kind) {
|
if lineShapeKind(kind) {
|
||||||
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
|
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
|
||||||
} else {
|
} else {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object.PathObject, err = object.PathObject.Reshape(inverse.TransformBox(box))
|
object.PathObject, err = object.PathObject.Reshape(inverse.TransformBox(box))
|
||||||
|
|||||||
@@ -16,19 +16,21 @@ package ofdgo
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// Style 获取内部成员的独立样式快照,路径CTM只表达页面描边倍率
|
// Style 获取内部成员的独立样式快照,文字和路径CTM只表达页面描边倍率
|
||||||
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
|
||||||
// 返回: GraphicObject 有效样式来源
|
// 返回: GraphicObject 有效样式来源
|
||||||
func (m CompositeMember) Style() GraphicObject {
|
func (m CompositeMember) Style() GraphicObject {
|
||||||
object := cloneEditorData(m.Object)
|
object := cloneEditorData(m.Object)
|
||||||
if object.Type == "PathObject" {
|
if object.Type == "PathObject" {
|
||||||
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
object.PathObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
||||||
|
} else if object.Type == "TextObject" {
|
||||||
|
object.TextObject.CTM = Matrix{a: m.StrokeScale, d: m.StrokeScale}.String()
|
||||||
}
|
}
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
|
||||||
// 路径复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
// 描边复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
|
||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, source GraphicObject) error {
|
||||||
@@ -36,6 +38,14 @@ func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, so
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
source, err = e.explicitDefaultPaint(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
member := members[i]
|
member := members[i]
|
||||||
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
if node.object.Type != source.Type || !member.Capabilities.Transform || source.Type != "ImageObject" && !member.Capabilities.Paint {
|
||||||
@@ -52,8 +62,11 @@ func (e *Editor) CopyCompositeStyle(page int, path ObjectPath, indexes []int, so
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object.TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
|
style := copyEditorPathStyle(object.TextObject.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/member.StrokeScale)
|
||||||
object.TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
|
if err := validateEditorStroke(style); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object.TextObject = object.TextObject.withPathStyle(style)
|
||||||
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
object.TextObject.DrawParam, err = e.neutralDrawParam()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateCompositeText 修改内部普通横向文字,原文保留定位,已知段落按会话选项重排
|
// UpdateCompositeText 修改内部文字,原文保留定位,已知段落按会话选项重排
|
||||||
// 空样式保留原字体、字号和颜色,显式改变字号时重新排版并保留首行基线
|
// 空样式保留原字体、字号和颜色,显式改变字号时重新排版并保留首行基线
|
||||||
|
// 方向和字形映射沿用RewriteText的定位校验,不改变原文中未修改的属性
|
||||||
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value string, style TextStyle) error {
|
func (e *Editor) UpdateCompositeText(page int, path ObjectPath, index int, value string, style TextStyle) error {
|
||||||
@@ -69,7 +70,8 @@ func (e *Editor) changeCompositeText(page int, path ObjectPath, indexes []int, v
|
|||||||
// 入参: node 原文节点, member 有效快照, value 新内容, style 样式增量, options 段落选项, frame 文字框调整
|
// 入参: node 原文节点, member 有效快照, value 新内容, style 样式增量, options 段落选项, frame 文字框调整
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) updateCompositeText(node *editorCompositeNode, member CompositeMember, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
func (e *Editor) updateCompositeText(node *editorCompositeNode, member CompositeMember, value *string, style TextStyle, options *TextLayout, frame *Box) error {
|
||||||
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont {
|
rewriteOnly := value != nil && options == nil && frame == nil && style.Color == "" && (style.Font == "" || style.Font == member.Object.TextObject.Font) && (style.Size == 0 || style.Size == member.Object.TextObject.Size)
|
||||||
|
if node.object.Type != "TextObject" || !member.Capabilities.ReplaceFont && !(rewriteOnly && member.Capabilities.RewriteText) {
|
||||||
return fmt.Errorf("composite text style is not supported")
|
return fmt.Errorf("composite text style is not supported")
|
||||||
}
|
}
|
||||||
object := cloneEditorData(node.object)
|
object := cloneEditorData(node.object)
|
||||||
|
|||||||
+76
-22
@@ -78,18 +78,31 @@ func (e *Editor) DocumentInfo(index int) (DocInfo, error) {
|
|||||||
// 入参: index 文档索引,从0开始, info 文档信息
|
// 入参: index 文档索引,从0开始, info 文档信息
|
||||||
// 返回: error 修改错误
|
// 返回: error 修改错误
|
||||||
func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
||||||
|
return e.SetDocumentInfoContext(context.Background(), index, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDocumentInfoContext 修改文档信息,取消或失败时保留元数据和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, index 文档索引,从0开始, info 文档信息
|
||||||
|
// 返回: error 修改或取消错误
|
||||||
|
func (e *Editor) SetDocumentInfoContext(ctx context.Context, index int, info DocInfo) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
before, err := e.DocumentInfo(index)
|
before, err := e.DocumentInfo(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if reflect.DeepEqual(before, info) {
|
if reflect.DeepEqual(before, info) {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
if index == e.DocumentIndex() {
|
if index == e.DocumentIndex() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
e.SetInfo(info)
|
e.SetInfo(info)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) {
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, _ *editorXML) (int, error) {
|
||||||
updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info)
|
updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -104,6 +117,16 @@ func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
|||||||
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
||||||
// 返回: int 新文档索引, error 创建错误
|
// 返回: int 新文档索引, error 创建错误
|
||||||
func (e *Editor) AddDocument(title string, width, height float64, at int) (int, error) {
|
func (e *Editor) AddDocument(title string, width, height float64, at int) (int, error) {
|
||||||
|
return e.AddDocumentContext(context.Background(), title, width, height, at)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDocumentContext 插入并选中空白文档,取消或失败时保留原包和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
||||||
|
// 返回: int 新文档索引, error 创建或取消错误
|
||||||
|
func (e *Editor) AddDocumentContext(ctx context.Context, title string, width, height float64, at int) (int, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if at < 0 || at > e.DocumentCount() {
|
if at < 0 || at > e.DocumentCount() {
|
||||||
return 0, fmt.Errorf("document index %d out of range", at)
|
return 0, fmt.Errorf("document index %d out of range", at)
|
||||||
}
|
}
|
||||||
@@ -112,25 +135,34 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
|||||||
if _, err := created.AddPage(width, height); err != nil {
|
if _, err := created.AddPage(width, height); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err := e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
err := e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
occupied := make(map[string]bool)
|
occupied := make(map[string]bool)
|
||||||
for name := range reader.fileNames {
|
for name := range reader.fileNames {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
directory, _, _ := strings.Cut(name, "/")
|
directory, _, _ := strings.Cut(name, "/")
|
||||||
occupied[directory] = true
|
occupied[directory] = true
|
||||||
}
|
}
|
||||||
directory := ""
|
directory := ""
|
||||||
for number := 0; ; number++ {
|
for number := 0; ; number++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
candidate := fmt.Sprintf("Doc_%d", number)
|
candidate := fmt.Sprintf("Doc_%d", number)
|
||||||
if !occupied[candidate] {
|
if !occupied[candidate] {
|
||||||
directory = candidate
|
directory = candidate
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createdReader, err := created.Reader()
|
createdReader, err := created.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
for name, content := range createdReader.files {
|
for name, content := range createdReader.files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if name == "OFD.xml" {
|
if name == "OFD.xml" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -168,33 +200,59 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
|||||||
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
|
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
|
||||||
// 返回: error 修改错误
|
// 返回: error 修改错误
|
||||||
func (e *Editor) RenameDocument(index int, title string) error {
|
func (e *Editor) RenameDocument(index int, title string) error {
|
||||||
|
return e.RenameDocumentContext(context.Background(), index, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameDocumentContext 修改文档标题,取消或失败时保留其他元数据和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, index 文档索引,从0开始, title 标题,空值移除标题
|
||||||
|
// 返回: error 修改或取消错误
|
||||||
|
func (e *Editor) RenameDocumentContext(ctx context.Context, index int, title string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
info, err := e.DocumentInfo(index)
|
info, err := e.DocumentInfo(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
info.Title = title
|
info.Title = title
|
||||||
return e.SetDocumentInfo(index, info)
|
return e.SetDocumentInfoContext(ctx, index, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
|
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
|
||||||
// 入参: from 原文档索引, to 目标索引,均从0开始
|
// 入参: from 原文档索引, to 目标索引,均从0开始
|
||||||
// 返回: error 移动错误
|
// 返回: error 移动错误
|
||||||
func (e *Editor) MoveDocument(from, to int) error {
|
func (e *Editor) MoveDocument(from, to int) error {
|
||||||
|
return e.MoveDocumentContext(context.Background(), from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveDocumentContext 调整文档顺序,取消或失败时保留当前文档和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, from 原文档索引, to 目标索引,均从0开始
|
||||||
|
// 返回: error 移动或取消错误
|
||||||
|
func (e *Editor) MoveDocumentContext(ctx context.Context, from, to int) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if from < 0 || from >= e.DocumentCount() || to < 0 || to >= e.DocumentCount() {
|
if from < 0 || from >= e.DocumentCount() || to < 0 || to >= e.DocumentCount() {
|
||||||
return fmt.Errorf("document index out of range")
|
return fmt.Errorf("document index out of range")
|
||||||
}
|
}
|
||||||
if from == to {
|
if from == to {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
bodies := make([][]byte, reader.DocumentCount())
|
bodies := make([][]byte, reader.DocumentCount())
|
||||||
for index := range bodies {
|
for index := range bodies {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
body := root.childAt("DocBody", index)
|
body := root.childAt("DocBody", index)
|
||||||
bodies[index] = data[body.start:body.end]
|
bodies[index] = data[body.start:body.end]
|
||||||
}
|
}
|
||||||
moveEditorItem(bodies, from, to)
|
moveEditorItem(bodies, from, to)
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
for index, content := range bodies {
|
for index, content := range bodies {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
body := root.childAt("DocBody", index)
|
body := root.childAt("DocBody", index)
|
||||||
patches = append(patches, editorXMLPatch{body.start, body.end, content})
|
patches = append(patches, editorXMLPatch{body.start, body.end, content})
|
||||||
}
|
}
|
||||||
@@ -295,17 +353,6 @@ func (e *Editor) DeleteDocumentContext(ctx context.Context, index int) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// editDocuments 在独立包快照中修改文档入口,失败时不改变当前状态或历史
|
|
||||||
// 入参: edit 修改包快照并返回新的当前文档索引
|
|
||||||
// 返回: error 修改错误
|
|
||||||
func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, error)) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
return e.editDocumentsContext(ctx, edit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// editDocumentsContext 在独立快照中修改文档入口,确认未取消后提交完整状态
|
// editDocumentsContext 在独立快照中修改文档入口,确认未取消后提交完整状态
|
||||||
// 入参: ctx 取消上下文, edit 修改包快照并返回新的当前文档索引
|
// 入参: ctx 取消上下文, edit 修改包快照并返回新的当前文档索引
|
||||||
// 返回: error 修改或取消错误
|
// 返回: error 修改或取消错误
|
||||||
@@ -314,6 +361,13 @@ func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return e.editDocumentSnapshot(ctx, reader, edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editDocumentSnapshot 修改已有独立快照的文档入口,成功后提交并记录撤销历史
|
||||||
|
// 入参: ctx 取消上下文, reader 包含当前编辑内容的独立快照, edit 修改快照并返回当前文档索引
|
||||||
|
// 返回: error 修改或取消错误
|
||||||
|
func (e *Editor) editDocumentSnapshot(ctx context.Context, reader *Reader, edit func(*Reader, []byte, *editorXML) (int, error)) error {
|
||||||
data, err := reader.readFileViewContext(ctx, "OFD.xml")
|
data, err := reader.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -340,11 +394,11 @@ func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []
|
|||||||
if previous.DocRoot != current.DocRoot || previous.DocInfo.DocID != current.DocInfo.DocID {
|
if previous.DocRoot != current.DocRoot || previous.DocInfo.DocID != current.DocInfo.DocID {
|
||||||
reader.selectedVersion = nil
|
reader.selectedVersion = nil
|
||||||
}
|
}
|
||||||
if _, err := reader.Doc(); err != nil {
|
if _, err := reader.DocContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
next := *e
|
next := *e
|
||||||
if err := next.adoptDocumentReader(reader); err != nil {
|
if err := next.adoptDocumentReader(ctx, reader); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -356,9 +410,9 @@ func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []
|
|||||||
}
|
}
|
||||||
|
|
||||||
// adoptDocumentReader 接入已加载的编辑入口,跨文档或版本时重建页面与资源状态
|
// adoptDocumentReader 接入已加载的编辑入口,跨文档或版本时重建页面与资源状态
|
||||||
// 入参: reader 不拥有输入文件的独立阅读器
|
// 入参: ctx 取消上下文, reader 不拥有输入文件的独立阅读器
|
||||||
// 返回: error 编辑器初始化错误
|
// 返回: error 编辑器初始化错误
|
||||||
func (e *Editor) adoptDocumentReader(reader *Reader) error {
|
func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error {
|
||||||
version, err := reader.DocumentVersionID()
|
version, err := reader.DocumentVersionID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -372,7 +426,7 @@ func (e *Editor) adoptDocumentReader(reader *Reader) error {
|
|||||||
source.reader = reader
|
source.reader = reader
|
||||||
e.source = &source
|
e.source = &source
|
||||||
} else {
|
} else {
|
||||||
next, err := reader.Editor()
|
next, err := reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的擦除范围
|
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的擦除范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.eraseObjects(page, ids, box, nil, nil)
|
return e.eraseObjects(page, ids, box, nil, nil)
|
||||||
@@ -55,7 +55,7 @@ func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error
|
|||||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, box 页面毫米范围
|
// 入参: page 页面索引, path 父路径, indexes 成员序号, box 页面毫米范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
|
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -530,7 +531,7 @@ func (e *Editor) restoreFontSnapshot(state Editor) {
|
|||||||
*e = state
|
*e = state
|
||||||
}
|
}
|
||||||
|
|
||||||
// pruneExternalFontFiles 清理全包无引用字体并同步版本清单,保留附件及无法确认的扩展引用
|
// pruneExternalFontFiles 流式核对全包字体引用并同步版本清单,保留附件及无法确认的扩展引用
|
||||||
// 入参: ctx 取消上下文, reader 包快照, candidates 待清理的字体路径
|
// 入参: ctx 取消上下文, reader 包快照, candidates 待清理的字体路径
|
||||||
// 返回: error 取消或读取错误
|
// 返回: error 取消或读取错误
|
||||||
func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...string) error {
|
func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...string) error {
|
||||||
@@ -559,7 +560,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
}
|
}
|
||||||
structures["OFD.xml"] = true
|
structures["OFD.xml"] = true
|
||||||
files := make(map[string]bool)
|
files := make(map[string]bool)
|
||||||
versions := make(map[string]*editorFontXML)
|
versions := make(map[string][]versionEditLink)
|
||||||
names := slices.Sorted(maps.Keys(structures))
|
names := slices.Sorted(maps.Keys(structures))
|
||||||
for _, name := range slices.Sorted(maps.Keys(reader.fileNames)) {
|
for _, name := range slices.Sorted(maps.Keys(reader.fileNames)) {
|
||||||
if !structures[name] && strings.HasSuffix(strings.ToLower(name), ".xml") {
|
if !structures[name] && strings.HasSuffix(strings.ToLower(name), ".xml") {
|
||||||
@@ -573,55 +574,46 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
if files[name] && !structures[name] {
|
if files[name] && !structures[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data, err := reader.readFileViewContext(ctx, name)
|
root, uncertain := "", false
|
||||||
if err != nil {
|
inspect := func(node xml.StartElement) error {
|
||||||
return err
|
if root == "" {
|
||||||
|
root = node.Name.Local
|
||||||
}
|
}
|
||||||
root, err := parseEditorXMLContext(ctx, data)
|
if classifyOFDNamespace(node.Name.Space) == ofdXMLUnknown || slices.Contains([]string{"CustomTags", "Extensions", "ExtendData", "Data"}, node.Name.Local) {
|
||||||
if err != nil {
|
uncertain = true
|
||||||
return ctx.Err()
|
return fmt.Errorf("unknown font file dependencies in %s", name)
|
||||||
}
|
}
|
||||||
base := ""
|
for _, attr := range node.Attr {
|
||||||
if root.matchesOFD("Res") {
|
|
||||||
base = root.attr("BaseLoc")
|
|
||||||
}
|
|
||||||
if root.matchesOFD("DocVersion") {
|
|
||||||
versions[name] = &editorFontXML{name: name, data: data, root: root}
|
|
||||||
}
|
|
||||||
var retains func(*editorXML) bool
|
|
||||||
retains = func(node *editorXML) bool {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown || slices.Contains([]string{"CustomTags", "Extensions", "ExtendData", "Data"}, node.name.Local) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
parent := ""
|
|
||||||
if node.parent != nil {
|
|
||||||
parent = node.parent.name.Local
|
|
||||||
}
|
|
||||||
for _, attr := range node.attrs {
|
|
||||||
if attr.Name.Space != "" && attr.Name.Space != "xmlns" && attr.Name.Space != "http://www.w3.org/XML/1998/namespace" {
|
if attr.Name.Space != "" && attr.Name.Space != "xmlns" && attr.Name.Space != "http://www.w3.org/XML/1998/namespace" {
|
||||||
return true
|
uncertain = true
|
||||||
|
return fmt.Errorf("unknown font file dependencies in %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
add := func(location string, kind int) {
|
||||||
|
location = cleanPackagePath(location)
|
||||||
|
if root == "DocVersion" && kind == packageReferenceData {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if kind := packageReference(root.name.Local, parent, node.name.Local, attr.Name.Local); attr.Name.Space == "" && kind != packageReferenceNone {
|
|
||||||
location := cleanPackagePath(resolveResourcePath(name, base, attr.Value))
|
|
||||||
delete(removed, location)
|
delete(removed, location)
|
||||||
if kind == packageReferenceData {
|
if kind == packageReferenceData {
|
||||||
files[location] = true
|
files[location] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
record := func(node *editorXML, attribute, target string) {
|
||||||
if kind := packageReference(root.name.Local, parent, node.name.Local, ""); kind != packageReferenceNone && !(root.matchesOFD("DocVersion") && parent == "FileList" && node.matchesOFD("File")) {
|
if root == "DocVersion" && node.name.Local == "File" && attribute == "" && removed[target] {
|
||||||
location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node)))
|
versions[name] = append(versions[name], versionEditLink{node: node, target: target})
|
||||||
delete(removed, cleanPackagePath(location))
|
|
||||||
if kind == packageReferenceData {
|
|
||||||
files[cleanPackagePath(location)] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return slices.ContainsFunc(node.children, retains)
|
if err := reader.scanDocumentFileReferences(ctx, name, true, add, record, inspect); err != nil {
|
||||||
|
var syntax *xml.SyntaxError
|
||||||
|
if uncertain || errors.As(err, &syntax) {
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
if retains(root) || len(removed) == 0 {
|
return err
|
||||||
|
}
|
||||||
|
if len(removed) == 0 {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,23 +621,25 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updates := make(map[string][]byte)
|
updates := make(map[string][]byte)
|
||||||
for name, file := range versions {
|
for name, links := range versions {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
if list := file.root.child("FileList"); list != nil {
|
for _, link := range links {
|
||||||
for _, node := range list.children {
|
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if node.matchesOFD("File") && removed[resolveResourcePath(name, "", strings.TrimSpace(editorImportText(file.data, node)))] {
|
if removed[link.target] {
|
||||||
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
patches = append(patches, editorXMLPatch{start: link.node.start, end: link.node.end})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(patches) != 0 {
|
if len(patches) != 0 {
|
||||||
updates[name] = editorPatchXML(file.data, patches)
|
data, err := reader.readFileViewContext(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updates[name] = editorPatchXML(data, patches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (e *Editor) compactVersionFonts(ctx context.Context, parts map[string][]byt
|
|||||||
if !versioned {
|
if !versioned {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
protected, err := reader.hasOutputSignatures()
|
protected, err := reader.hasOutputSignatures(ctx)
|
||||||
if err != nil || protected {
|
if err != nil || protected {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-6
@@ -1212,6 +1212,15 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
var content []byte
|
var content []byte
|
||||||
pageContent := name == "Page" && node.parent == nil
|
pageContent := name == "Page" && node.parent == nil
|
||||||
if pageContent {
|
if pageContent {
|
||||||
|
for _, child := range node.children {
|
||||||
|
if child.matchesOFD("PageRes") || !entry.template && child.matchesOFD("Template") {
|
||||||
|
encoded, err := m.encode(entry, child)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content = append(content, encoded...)
|
||||||
|
}
|
||||||
|
}
|
||||||
local := node.child("Area")
|
local := node.child("Area")
|
||||||
if local == nil {
|
if local == nil {
|
||||||
var templates []Template
|
var templates []Template
|
||||||
@@ -1234,20 +1243,17 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
content = encoded
|
content = append(content, encoded...)
|
||||||
} else {
|
} else {
|
||||||
encoded, err := m.encode(entry, local)
|
encoded, err := m.encode(entry, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
content = encoded
|
content = append(content, encoded...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
if pageContent && entry.template && child.matchesOFD("Template") {
|
if pageContent && (child.matchesOFD("Area") || child.matchesOFD("Template") || child.matchesOFD("PageRes")) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pageContent && child.name.Local == "Area" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if name == "Goto" && child.name.Local == "Bookmark" {
|
if name == "Goto" && child.name.Local == "Bookmark" {
|
||||||
|
|||||||
+20
-1
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -117,7 +118,15 @@ type PageCapabilities struct {
|
|||||||
// 版本共享文件在提交修改时隔离,保留其他版本及其文件清单
|
// 版本共享文件在提交修改时隔离,保留其他版本及其文件清单
|
||||||
// 返回: *Editor 编辑器, error 错误信息
|
// 返回: *Editor 编辑器, error 错误信息
|
||||||
func (r *Reader) Editor() (*Editor, error) {
|
func (r *Reader) Editor() (*Editor, error) {
|
||||||
_, err := r.Doc()
|
return r.EditorContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditorContext 打开当前文档进行保真编辑,读取及构建页面索引时响应取消
|
||||||
|
// 编辑器及其快照使用期间不得关闭输入Reader,不修改输入包或限制编辑权限
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Editor 编辑器, error 初始化或取消错误
|
||||||
|
func (r *Reader) EditorContext(ctx context.Context) (*Editor, error) {
|
||||||
|
_, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -144,6 +153,9 @@ func (r *Reader) Editor() (*Editor, error) {
|
|||||||
e.pages = make([]PageContent, len(doc.Pages.Page))
|
e.pages = make([]PageContent, len(doc.Pages.Page))
|
||||||
sourcePages := make([]editorSourcePage, len(doc.Pages.Page))
|
sourcePages := make([]editorSourcePage, len(doc.Pages.Page))
|
||||||
for i, page := range doc.Pages.Page {
|
for i, page := range doc.Pages.Page {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if page.ID == "" || e.source.pages[page.ID] != nil {
|
if page.ID == "" || e.source.pages[page.ID] != nil {
|
||||||
return nil, fmt.Errorf("missing or duplicate page ID %q", page.ID)
|
return nil, fmt.Errorf("missing or duplicate page ID %q", page.ID)
|
||||||
}
|
}
|
||||||
@@ -151,6 +163,9 @@ func (r *Reader) Editor() (*Editor, error) {
|
|||||||
e.source.pages[page.ID] = &sourcePages[i]
|
e.source.pages[page.ID] = &sourcePages[i]
|
||||||
e.pages[i] = PageContent{ID: page.ID}
|
e.pages[i] = PageContent{ID: page.ID}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,11 +661,15 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
|||||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
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.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
||||||
a.LineWidthSet = b.LineWidthSet
|
a.LineWidthSet = b.LineWidthSet
|
||||||
|
a.MiterLimit, a.MiterLimitSet = b.MiterLimit, b.MiterLimitSet
|
||||||
|
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
||||||
|
a.dashPatternSet = b.dashPatternSet
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
a, b := &before.PathObject, after.PathObject
|
a, b := &before.PathObject, after.PathObject
|
||||||
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
|
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.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
|
||||||
a.LineWidthSet = b.LineWidthSet
|
a.LineWidthSet = b.LineWidthSet
|
||||||
|
a.MiterLimit, a.MiterLimitSet = b.MiterLimit, b.MiterLimitSet
|
||||||
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
a.DashOffset, a.DashPattern, a.Cap, a.Join = b.DashOffset, b.DashPattern, b.Cap, b.Join
|
||||||
a.dashPatternSet = b.dashPatternSet
|
a.dashPatternSet = b.dashPatternSet
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
|
|||||||
+18
-6
@@ -340,11 +340,23 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
|||||||
}
|
}
|
||||||
patches = append(patches, editorXMLPatch{area.start, area.end, updated})
|
patches = append(patches, editorXMLPatch{area.start, area.end, updated})
|
||||||
} else {
|
} else {
|
||||||
value, err := source.root.containerXML("Area", nil, source.root.textXML("PhysicalBox", page.Area.PhysicalBox))
|
var fields []byte
|
||||||
|
for _, field := range [][2]string{{"PhysicalBox", page.Area.PhysicalBox}, {"ApplicationBox", page.Area.ApplicationBox}, {"ContentBox", page.Area.ContentBox}, {"BleedBox", page.Area.BleedBox}} {
|
||||||
|
if field[1] != "" {
|
||||||
|
fields = append(fields, source.root.textXML(field[0], field[1])...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value, err := source.root.containerXML("Area", nil, fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
patches = append(patches, editorXMLPatch{source.root.open, source.root.open, value})
|
position := source.root.open
|
||||||
|
for _, child := range source.root.children {
|
||||||
|
if child.matchesOFD("Template") || child.matchesOFD("PageRes") {
|
||||||
|
position = child.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{position, position, value})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for layerIndex, original := range source.original.Content.Layer {
|
for layerIndex, original := range source.original.Content.Layer {
|
||||||
@@ -546,7 +558,7 @@ func (e *Editor) sourcePageData(page PageContent, references *editorReferenceSca
|
|||||||
err := x.finish()
|
err := x.finish()
|
||||||
return bytes.TrimPrefix(data.Bytes(), []byte(xml.Header)), err
|
return bytes.TrimPrefix(data.Bytes(), []byte(xml.Header)), err
|
||||||
}
|
}
|
||||||
area, err := editorXMLContainer("Area", nil, editorXMLText("PhysicalBox", page.Area.PhysicalBox))
|
header, err := e.encodeXML(func(x *ofdXML) { x.pageHeader(page) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -562,7 +574,7 @@ func (e *Editor) sourcePageData(page PageContent, references *editorReferenceSca
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data := append(area, content...)
|
data := append(bytes.TrimPrefix(header, []byte(xml.Header)), content...)
|
||||||
if len(page.Actions) != 0 {
|
if len(page.Actions) != 0 {
|
||||||
actions, err := e.encodeXML(func(x *ofdXML) { x.actions(page.Actions) })
|
actions, err := e.encodeXML(func(x *ofdXML) { x.actions(page.Actions) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -754,7 +766,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
|
|||||||
delete(reader.pageHeaderCache, name)
|
delete(reader.pageHeaderCache, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := reader.Doc(); err != nil {
|
if _, err := reader.DocContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return reader, nil
|
return reader, nil
|
||||||
@@ -768,7 +780,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
|
|||||||
if err := reader.initRootContext(ctx); err != nil {
|
if err := reader.initRootContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := reader.Doc(); err != nil {
|
if _, err := reader.DocContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for name, header := range e.source.reader.pageHeaderCache {
|
for name, header := range e.source.reader.pageHeaderCache {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (e *Editor) FlipSelection(page int, objects, annotations []string, axis str
|
|||||||
// 入参: page 页面索引, objects 正文标识, annotations 注解标识, box 页面毫米目标范围
|
// 入参: page 页面索引, objects 正文标识, annotations 注解标识, box 页面毫米目标范围
|
||||||
// 返回: error 标识、度量或编辑错误
|
// 返回: error 标识、度量或编辑错误
|
||||||
func (e *Editor) ResizeSelection(page int, objects, annotations []string, box Box) error {
|
func (e *Editor) ResizeSelection(page int, objects, annotations []string, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bounds, err := e.SelectionBounds(page, objects, annotations)
|
bounds, err := e.SelectionBounds(page, objects, annotations)
|
||||||
|
|||||||
+82
-40
@@ -23,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
|
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
|
||||||
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
|
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的线宽、虚线长度及偏移使用页面毫米
|
||||||
type ObjectStyle struct {
|
type ObjectStyle struct {
|
||||||
Alpha *int
|
Alpha *int
|
||||||
Fill *bool
|
Fill *bool
|
||||||
@@ -31,6 +31,7 @@ type ObjectStyle struct {
|
|||||||
FillColor *FillColor
|
FillColor *FillColor
|
||||||
StrokeColor *StrokeColor
|
StrokeColor *StrokeColor
|
||||||
LineWidth *float64
|
LineWidth *float64
|
||||||
|
MiterLimit *float64
|
||||||
DashPattern *string
|
DashPattern *string
|
||||||
DashOffset *float64
|
DashOffset *float64
|
||||||
Cap *string
|
Cap *string
|
||||||
@@ -251,7 +252,7 @@ func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
|||||||
return e.addEditorDrawParam(cloneEditorData(draw))
|
return e.addEditorDrawParam(cloneEditorData(draw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -260,7 +261,7 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range objects {
|
for i := range objects {
|
||||||
if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil {
|
if style.Fill != nil || style.Stroke != nil || style.FillColor != nil || style.StrokeColor != nil || style.LineWidth != nil || style.MiterLimit != nil || style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != 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
|
||||||
@@ -278,8 +279,8 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
|
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
|
||||||
// 文字复制字体、字号、颜色及透明度,沿用目标段落设置;字体标识须属于当前文档
|
// 文字复制字体、字号、填充、描边及透明度,沿用目标段落设置;字体标识须属于当前文档
|
||||||
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
|
// 描边按页面中的实际线宽和虚线长度复制,保持目标变换
|
||||||
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
|
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err error) {
|
func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err error) {
|
||||||
@@ -306,30 +307,19 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if source.Type == "TextObject" && source.TextObject.FillColor == nil || source.Type == "PathObject" && (source.PathObject.FillColor == nil || source.PathObject.StrokeColor == nil) {
|
source, err = e.explicitDefaultPaint(source)
|
||||||
black, err := e.RGBColor(color.NRGBA{A: 255})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if source.Type == "TextObject" {
|
|
||||||
source.TextObject.FillColor = black
|
|
||||||
} else {
|
|
||||||
if source.PathObject.FillColor == nil {
|
|
||||||
source.PathObject.FillColor = black
|
|
||||||
}
|
|
||||||
if source.PathObject.StrokeColor == nil {
|
|
||||||
source.PathObject.StrokeColor = (*StrokeColor)(black)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if source.Type == "TextObject" {
|
if source.Type == "TextObject" {
|
||||||
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
|
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range objects {
|
for i := range objects {
|
||||||
objects[i].TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
|
text := objects[i].TextObject
|
||||||
objects[i].TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
|
style := copyEditorPathStyle(text.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/editorStrokeScale(text.CTM))
|
||||||
|
objects[i].TextObject = text.withPathStyle(style)
|
||||||
}
|
}
|
||||||
return e.UpdateObjects(page, objects)
|
return e.UpdateObjects(page, objects)
|
||||||
}
|
}
|
||||||
@@ -346,6 +336,44 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err er
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// explicitDefaultPaint 将样式快照的缺省颜色写为显式颜色,避免继承目标容器颜色
|
||||||
|
// 入参: object 已解析继承样式的文字或路径,其他类型保持不变
|
||||||
|
// 返回: GraphicObject 颜色完整的样式快照, error 颜色资源错误
|
||||||
|
func (e *Editor) explicitDefaultPaint(object GraphicObject) (GraphicObject, error) {
|
||||||
|
var fill **FillColor
|
||||||
|
var stroke **StrokeColor
|
||||||
|
text := object.Type == "TextObject"
|
||||||
|
if text {
|
||||||
|
fill, stroke = &object.TextObject.FillColor, &object.TextObject.StrokeColor
|
||||||
|
} else if object.Type == "PathObject" {
|
||||||
|
fill, stroke = &object.PathObject.FillColor, &object.PathObject.StrokeColor
|
||||||
|
} else {
|
||||||
|
return object, nil
|
||||||
|
}
|
||||||
|
if *fill != nil && *stroke != nil {
|
||||||
|
return object, nil
|
||||||
|
}
|
||||||
|
black, err := e.RGBColor(color.NRGBA{A: 255})
|
||||||
|
if err != nil {
|
||||||
|
return GraphicObject{}, err
|
||||||
|
}
|
||||||
|
transparent, zero := *black, 0
|
||||||
|
transparent.Alpha = &zero
|
||||||
|
if *fill == nil {
|
||||||
|
*fill = &transparent
|
||||||
|
if text {
|
||||||
|
*fill = black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *stroke == nil {
|
||||||
|
*stroke = (*StrokeColor)(black)
|
||||||
|
if text {
|
||||||
|
*stroke = (*StrokeColor)(&transparent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object, nil
|
||||||
|
}
|
||||||
|
|
||||||
// StyleImageBorders 原子修改图片边框,保留未修改的颜色、线型及原文扩展
|
// StyleImageBorders 原子修改图片边框,保留未修改的颜色、线型及原文扩展
|
||||||
// 入参: page 页面索引, ids 图片标识, style 边框增量
|
// 入参: page 页面索引, ids 图片标识, style 边框增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -498,26 +526,28 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
|
|||||||
if object.Type == "PathObject" {
|
if object.Type == "PathObject" {
|
||||||
obj := &object.PathObject
|
obj := &object.PathObject
|
||||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join, Cap: obj.Cap,
|
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join, Cap: obj.Cap,
|
||||||
MiterLimit: obj.MiterLimit, DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset,
|
MiterLimit: obj.MiterLimit, MiterLimitSet: obj.MiterLimitSet, DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset,
|
||||||
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
||||||
obj.DrawParam = ""
|
obj.DrawParam = ""
|
||||||
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
|
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
|
||||||
obj.LineWidthSet = style.LineWidthSet
|
obj.LineWidthSet = style.LineWidthSet
|
||||||
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
|
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
|
||||||
|
obj.MiterLimitSet = style.MiterLimitSet
|
||||||
obj.dashPatternSet = style.dashPatternSet
|
obj.dashPatternSet = style.dashPatternSet
|
||||||
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||||
} else {
|
} else {
|
||||||
obj := &object.TextObject
|
obj := &object.TextObject
|
||||||
if style.Cap != "" && style.Cap != "Butt" || style.DashPattern != "" || style.DashOffset != nil && *style.DashOffset != 0 {
|
|
||||||
return GraphicObject{}, &EditError{Code: EditUnsupportedStyle, Err: fmt.Errorf("text draw parameters require unsupported stroke styles")}
|
|
||||||
}
|
|
||||||
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join,
|
style = mergeDrawParam(*style, &DrawParam{LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet, Join: obj.Join,
|
||||||
MiterLimit: obj.MiterLimit, FillColor: obj.FillColor, StrokeColor: obj.StrokeColor})
|
MiterLimit: obj.MiterLimit, MiterLimitSet: obj.MiterLimitSet, FillColor: obj.FillColor, StrokeColor: obj.StrokeColor, Cap: obj.Cap,
|
||||||
|
DashPattern: obj.DashPattern, dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset})
|
||||||
text := mergeDrawParam(*dp, &DrawParam{Font: obj.Font, Size: obj.Size, Weight: obj.Weight, Italic: obj.Italic})
|
text := mergeDrawParam(*dp, &DrawParam{Font: obj.Font, Size: obj.Size, Weight: obj.Weight, Italic: obj.Italic})
|
||||||
obj.DrawParam = ""
|
obj.DrawParam = ""
|
||||||
obj.Font, obj.Size, obj.Weight, obj.Italic = text.Font, text.Size, text.Weight, text.Italic
|
obj.Font, obj.Size, obj.Weight, obj.Italic = text.Font, text.Size, text.Weight, text.Italic
|
||||||
obj.LineWidth, obj.Join, obj.MiterLimit = style.LineWidth, style.Join, style.MiterLimit
|
obj.LineWidth, obj.Join, obj.MiterLimit = style.LineWidth, style.Join, style.MiterLimit
|
||||||
|
obj.MiterLimitSet = style.MiterLimitSet
|
||||||
obj.LineWidthSet = style.LineWidthSet
|
obj.LineWidthSet = style.LineWidthSet
|
||||||
|
obj.Cap, obj.DashPattern, obj.DashOffset = style.Cap, style.DashPattern, style.DashOffset
|
||||||
|
obj.dashPatternSet = style.dashPatternSet
|
||||||
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)} {
|
||||||
@@ -535,10 +565,10 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
|||||||
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
|
if style.Alpha != nil && (*style.Alpha < 0 || *style.Alpha > 255) {
|
||||||
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
|
return GraphicObject{}, fmt.Errorf("alpha must be between 0 and 255")
|
||||||
}
|
}
|
||||||
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil
|
stroke := style.DashPattern != nil || style.DashOffset != nil || style.Cap != nil || style.Join != nil || style.MiterLimit != nil
|
||||||
paint := style.Fill != nil || style.Stroke != nil || style.StrokeColor != nil || style.LineWidth != nil
|
paint := style.Fill != nil || style.Stroke != nil || style.StrokeColor != nil || style.LineWidth != nil
|
||||||
if (stroke || paint) && object.Type != "PathObject" {
|
if (stroke || paint) && object.Type != "PathObject" && object.Type != "TextObject" {
|
||||||
return GraphicObject{}, fmt.Errorf("stroke style requires path objects")
|
return GraphicObject{}, fmt.Errorf("stroke style requires text or path objects")
|
||||||
}
|
}
|
||||||
if style.FillColor != nil && object.Type != "TextObject" && object.Type != "PathObject" {
|
if style.FillColor != nil && object.Type != "TextObject" && object.Type != "PathObject" {
|
||||||
return GraphicObject{}, fmt.Errorf("fill color requires text or path objects")
|
return GraphicObject{}, fmt.Errorf("fill color requires text or path objects")
|
||||||
@@ -551,13 +581,6 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
|
||||||
if style.Alpha != nil {
|
|
||||||
object.TextObject.Alpha = style.Alpha
|
|
||||||
}
|
|
||||||
if style.FillColor != nil {
|
|
||||||
object.TextObject.FillColor = editorStyleColor(style.FillColor, object.TextObject.FillColor)
|
|
||||||
}
|
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
object.ImageObject.Alpha = style.Alpha
|
object.ImageObject.Alpha = style.Alpha
|
||||||
@@ -566,8 +589,11 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
|||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
object.CompositeGraphicUnit.Alpha = style.Alpha
|
object.CompositeGraphicUnit.Alpha = style.Alpha
|
||||||
}
|
}
|
||||||
case "PathObject":
|
case "TextObject", "PathObject":
|
||||||
path := &object.PathObject
|
path := object.PathObject
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
path = object.TextObject.pathStyle()
|
||||||
|
}
|
||||||
if style.Alpha != nil {
|
if style.Alpha != nil {
|
||||||
path.Alpha = style.Alpha
|
path.Alpha = style.Alpha
|
||||||
}
|
}
|
||||||
@@ -603,11 +629,20 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
|
|||||||
if style.Join != nil {
|
if style.Join != nil {
|
||||||
path.Join = *style.Join
|
path.Join = *style.Join
|
||||||
}
|
}
|
||||||
|
if style.MiterLimit != nil {
|
||||||
|
path.MiterLimit = *style.MiterLimit
|
||||||
|
path.MiterLimitSet = path.MiterLimit == 0
|
||||||
|
}
|
||||||
if stroke {
|
if stroke {
|
||||||
if err := validateEditorStroke(*path); err != nil {
|
if err := validateEditorStroke(path); err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
object.TextObject = object.TextObject.withPathStyle(path)
|
||||||
|
} else {
|
||||||
|
object.PathObject = path
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
|
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
|
||||||
}
|
}
|
||||||
@@ -633,7 +668,7 @@ func (e *Editor) validateCopyStyle(source GraphicObject) error {
|
|||||||
var colors []*FillColor
|
var colors []*FillColor
|
||||||
switch source.Type {
|
switch source.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
alpha, colors = source.TextObject.Alpha, []*FillColor{source.TextObject.FillColor}
|
return e.validateCopyStyle(GraphicObject{Type: "PathObject", PathObject: source.TextObject.pathStyle()})
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
from := source.PathObject
|
from := source.PathObject
|
||||||
alpha, colors = from.Alpha, []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)}
|
alpha, colors = from.Alpha, []*FillColor{from.FillColor, (*FillColor)(from.StrokeColor)}
|
||||||
@@ -668,6 +703,7 @@ func copyEditorPathStyle(to, from PathObject, scale float64) PathObject {
|
|||||||
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
to.FillColor, to.StrokeColor = from.FillColor, from.StrokeColor
|
||||||
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
to.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
|
||||||
to.LineWidthSet = from.LineWidthSet
|
to.LineWidthSet = from.LineWidthSet
|
||||||
|
to.MiterLimitSet = from.MiterLimitSet
|
||||||
to.DashPattern, to.DashOffset, to.dashPatternSet = from.DashPattern, from.DashOffset, true
|
to.DashPattern, to.DashOffset, to.dashPatternSet = from.DashPattern, from.DashOffset, true
|
||||||
if to.LineWidth == 0 && !to.LineWidthSet {
|
if to.LineWidth == 0 && !to.LineWidthSet {
|
||||||
to.LineWidth = defaultPathLineWidth
|
to.LineWidth = defaultPathLineWidth
|
||||||
@@ -678,7 +714,7 @@ func copyEditorPathStyle(to, from PathObject, scale float64) PathObject {
|
|||||||
if to.Join == "" {
|
if to.Join == "" {
|
||||||
to.Join = "Miter"
|
to.Join = "Miter"
|
||||||
}
|
}
|
||||||
if to.MiterLimit == 0 {
|
if to.MiterLimit == 0 && !to.MiterLimitSet {
|
||||||
to.MiterLimit = defaultMiterLimit
|
to.MiterLimit = defaultMiterLimit
|
||||||
}
|
}
|
||||||
if to.DashOffset == nil {
|
if to.DashOffset == nil {
|
||||||
@@ -757,6 +793,9 @@ func validateEditorStroke(path PathObject) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(values) < 2 {
|
||||||
|
return fmt.Errorf("dash pattern requires at least two lengths")
|
||||||
|
}
|
||||||
total := 0.0
|
total := 0.0
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value < 0 {
|
if value < 0 {
|
||||||
@@ -767,5 +806,8 @@ func validateEditorStroke(path PathObject) error {
|
|||||||
if total == 0 {
|
if total == 0 {
|
||||||
return fmt.Errorf("dash pattern must have a positive length")
|
return fmt.Errorf("dash pattern must have a positive length")
|
||||||
}
|
}
|
||||||
|
if !finite(total) || len(values)%2 != 0 && !finite(total*2) {
|
||||||
|
return fmt.Errorf("dash period overflow")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+27
-4
@@ -81,11 +81,22 @@ func (e *MissingGlyphError) Error() string {
|
|||||||
// 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号
|
// 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号
|
||||||
// 入参: page 页面索引, ids 文字对象标识, style 样式增量
|
// 入参: page 页面索引, ids 文字对象标识, style 样式增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
func (e *Editor) StyleText(page int, ids []string, style TextStyle) (err error) {
|
||||||
objects, _, err := e.selectedObjects(page, ids)
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
count, maximum := len(e.resources), e.maxID
|
||||||
|
ready := e.source != nil && e.source.idsReady
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
clear(e.resources[count:])
|
||||||
|
e.resources, e.maxID = e.resources[:count], maximum
|
||||||
|
if e.source != nil {
|
||||||
|
e.source.idsReady = ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
updates, err := e.styleTextObjects(objects, style)
|
updates, err := e.styleTextObjects(objects, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -97,7 +108,7 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
|
|||||||
// 原文等长替换保留全部位移,单个TextCode内增删仅调整局部步进,跨定位段增删需显式重排
|
// 原文等长替换保留全部位移,单个TextCode内增删仅调整局部步进,跨定位段增删需显式重排
|
||||||
// 入参: page 页面索引, id 文字对象标识, value 新内容, style 样式增量
|
// 入参: page 页面索引, id 文字对象标识, value 新内容, style 样式增量
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle) error {
|
func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle) (err error) {
|
||||||
capability, err := e.ObjectCapabilities(page, id)
|
capability, err := e.ObjectCapabilities(page, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -118,6 +129,17 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
|||||||
}
|
}
|
||||||
return e.updateObjects(page, []GraphicObject{object}, true)
|
return e.updateObjects(page, []GraphicObject{object}, true)
|
||||||
}
|
}
|
||||||
|
count, maximum := len(e.resources), e.maxID
|
||||||
|
ready := e.source != nil && e.source.idsReady
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
clear(e.resources[count:])
|
||||||
|
e.resources, e.maxID = e.resources[:count], maximum
|
||||||
|
if e.source != nil {
|
||||||
|
e.source.idsReady = ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
updates, err := e.styleTextObjects([]GraphicObject{object}, style)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -178,10 +200,11 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
}
|
}
|
||||||
codes := append([]TextCode(nil), obj.TextCode...)
|
codes := append([]TextCode(nil), obj.TextCode...)
|
||||||
transforms := append([]CGTransform(nil), obj.CGTransform...)
|
transforms := append([]CGTransform(nil), obj.CGTransform...)
|
||||||
|
lines := newTextLineScanner(*obj)
|
||||||
if len(old) == len(next) {
|
if len(old) == len(next) {
|
||||||
offset := 0
|
offset := 0
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
if obj.textCodeLineBreak(i) {
|
if lines.next(codes[i]) {
|
||||||
if next[offset] != '\n' {
|
if next[offset] != '\n' {
|
||||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing text lines requires explicit layout")}
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing text lines requires explicit layout")}
|
||||||
}
|
}
|
||||||
@@ -212,7 +235,7 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
}
|
}
|
||||||
offset, changed := 0, false
|
offset, changed := 0, false
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
if obj.textCodeLineBreak(i) {
|
if lines.next(codes[i]) {
|
||||||
offset++
|
offset++
|
||||||
}
|
}
|
||||||
runes := textCodeRunes(codes[i].Value)
|
runes := textCodeRunes(codes[i].Value)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
|||||||
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的目标范围
|
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的目标范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
|
func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
objects, _, err := e.selectedObjects(page, ids)
|
objects, _, err := e.selectedObjects(page, ids)
|
||||||
@@ -123,10 +123,15 @@ func (obj ImageObject) ImageBounds() (Box, error) {
|
|||||||
if obj.CTM == "" {
|
if obj.CTM == "" {
|
||||||
return box, nil
|
return box, nil
|
||||||
}
|
}
|
||||||
if _, err := creationNumbers(obj.CTM, 6); err != nil {
|
values, err := creationNumbers(obj.CTM, 6)
|
||||||
|
if err != nil {
|
||||||
return Box{}, err
|
return Box{}, err
|
||||||
}
|
}
|
||||||
return TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(obj.CTM)).TransformBox(Box{W: 1, H: 1}), nil
|
box = TranslationMatrix(box.X, box.Y).Multiply(MatrixFromValues([6]float64(values))).TransformBox(Box{W: 1, H: 1})
|
||||||
|
if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || !finite(box.X+box.W) || !finite(box.Y+box.H) {
|
||||||
|
return Box{}, fmt.Errorf("image bounds exceed finite range")
|
||||||
|
}
|
||||||
|
return box, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
|
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
|
||||||
@@ -519,21 +524,35 @@ func cropImageObject(image *ImageObject, box Box) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if box.X < full.X-1e-9 || box.Y < full.Y-1e-9 || box.X+box.W > full.X+full.W+1e-9 || box.Y+box.H > full.Y+full.H+1e-9 {
|
if box.X < full.X-1e-9 || box.Y < full.Y-1e-9 || box.X+box.W > full.X+full.W+1e-9 || box.Y+box.H > full.Y+full.H+1e-9 {
|
||||||
return fmt.Errorf("crop must stay within the image bounds")
|
return fmt.Errorf("crop must stay within the image bounds")
|
||||||
}
|
}
|
||||||
before, _ := ParseBox(image.Boundary)
|
before, _ := ParseBox(image.Boundary)
|
||||||
|
matrix := NewMatrix(image.CTM)
|
||||||
if image.CTM == "" {
|
if image.CTM == "" {
|
||||||
image.CTM = Matrix{a: before.W, d: before.H}.String()
|
matrix = Matrix{a: before.W, d: before.H}
|
||||||
}
|
}
|
||||||
if box.X != before.X || box.Y != before.Y {
|
if box.X != before.X || box.Y != before.Y {
|
||||||
image.CTM = TranslationMatrix(before.X-box.X, before.Y-box.Y).Multiply(NewMatrix(image.CTM)).String()
|
x, y := matrix.e+(before.X-box.X), matrix.f+(before.Y-box.Y)
|
||||||
|
if !finite(x) {
|
||||||
|
x = before.X + matrix.e - box.X
|
||||||
}
|
}
|
||||||
image.Boundary = editorBoxString(box)
|
if !finite(y) {
|
||||||
image.Clips = nil
|
y = before.Y + matrix.f - box.Y
|
||||||
|
}
|
||||||
|
matrix.e, matrix.f = x, y
|
||||||
|
}
|
||||||
|
if !finite(matrix.e) || !finite(matrix.f) {
|
||||||
|
return fmt.Errorf("crop transform exceeds finite range")
|
||||||
|
}
|
||||||
|
next := *image
|
||||||
|
if image.CTM == "" || box.X != before.X || box.Y != before.Y {
|
||||||
|
next.CTM = matrix.String()
|
||||||
|
}
|
||||||
|
next.Boundary, next.Clips = editorBoxString(box), nil
|
||||||
if box != full {
|
if box != full {
|
||||||
path, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
|
path, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -541,8 +560,9 @@ func cropImageObject(image *ImageObject, box Box) error {
|
|||||||
}
|
}
|
||||||
fill, stroke := true, false
|
fill, stroke := true, false
|
||||||
path.Fill, path.Stroke = &fill, &stroke
|
path.Fill, path.Stroke = &fill, &stroke
|
||||||
image.Clips = &Clips{Clip: []Clip{{Area: []ClipArea{{Path: []PathObject{path}}}}}}
|
next.Clips = &Clips{Clip: []Clip{{Area: []ClipArea{{Path: []PathObject{path}}}}}}
|
||||||
}
|
}
|
||||||
|
*image = next
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+132
-19
@@ -29,14 +29,24 @@ import (
|
|||||||
// DocumentVersions 获取当前编辑文档的版本信息,包含尚未写出的修改
|
// DocumentVersions 获取当前编辑文档的版本信息,包含尚未写出的修改
|
||||||
// 返回: []DocumentVersionInfo 版本信息, error 读取错误
|
// 返回: []DocumentVersionInfo 版本信息, error 读取错误
|
||||||
func (e *Editor) DocumentVersions() ([]DocumentVersionInfo, error) {
|
func (e *Editor) DocumentVersions() ([]DocumentVersionInfo, error) {
|
||||||
|
return e.DocumentVersionsContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentVersionsContext 获取包含未保存修改的版本信息,支持取消快照准备及描述读取
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: []DocumentVersionInfo 版本信息, error 读取或取消错误
|
||||||
|
func (e *Editor) DocumentVersionsContext(ctx context.Context) ([]DocumentVersionInfo, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
reader, err := e.Reader()
|
reader, err := e.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return reader.DocumentVersions()
|
return reader.DocumentVersionsContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentVersionID 获取当前编辑版本的标识,主入口返回空字符串
|
// DocumentVersionID 获取当前编辑版本的标识,主入口返回空字符串
|
||||||
@@ -53,22 +63,37 @@ func (e *Editor) DocumentVersionID() (string, error) {
|
|||||||
// 入参: id 版本标识
|
// 入参: id 版本标识
|
||||||
// 返回: error 切换错误
|
// 返回: error 切换错误
|
||||||
func (e *Editor) SelectVersion(id string) error {
|
func (e *Editor) SelectVersion(id string) error {
|
||||||
|
return e.SelectVersionContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectVersionContext 切换编辑版本并保留未保存修改,取消或失败时不改变编辑状态和历史
|
||||||
|
// 入参: ctx 取消上下文, id 版本标识,空值选择主入口
|
||||||
|
// 返回: error 切换或取消错误
|
||||||
|
func (e *Editor) SelectVersionContext(ctx context.Context, id string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
current, err := e.DocumentVersionID()
|
current, err := e.DocumentVersionID()
|
||||||
if err != nil || current == id {
|
if err != nil || current == id {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reader, err := e.Reader()
|
reader, err := e.readerContext(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
view, err := reader.DocumentVersion(id)
|
view, err := reader.DocumentVersionContext(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
before := e.transactionSnapshot()
|
before := e.transactionSnapshot()
|
||||||
if err := e.adoptDocumentReader(view); err != nil {
|
next := *e
|
||||||
|
if err := next.adoptDocumentReader(ctx, view); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*e = next
|
||||||
e.recordTransaction(before)
|
e.recordTransaction(before)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -180,7 +205,7 @@ func (e *Editor) AddVersionContext(ctx context.Context, name string) (string, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
data, err = editorVersionDefault(data, body, "")
|
data, err = editorVersionDefault(ctx, data, body, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -212,16 +237,42 @@ func (e *Editor) AddVersionContext(ctx context.Context, name string) (string, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultVersion 设置下次打开使用的版本,不切换当前编辑入口,可通过Undo撤销
|
// SetDefaultVersion 设置下次打开使用的版本,不切换当前编辑入口,可通过Undo撤销
|
||||||
// 空标识取消默认版本并使用主入口;修改OFD.xml可能使已有签名失效
|
// 空标识取消默认版本并使用主入口,无主入口时不可取消;修改OFD.xml可能使已有签名失效
|
||||||
// 入参: id 版本标识
|
// 入参: id 版本标识
|
||||||
// 返回: error 设置错误
|
// 返回: error 设置错误
|
||||||
func (e *Editor) SetDefaultVersion(id string) error {
|
func (e *Editor) SetDefaultVersion(id string) error {
|
||||||
versions, err := e.DocumentVersions()
|
return e.SetDefaultVersionContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultVersionContext 修改默认版本,取消或失败时保留编辑入口和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, id 版本标识,空值取消默认版本
|
||||||
|
// 返回: error 设置或取消错误
|
||||||
|
func (e *Editor) SetDefaultVersionContext(ctx context.Context, id string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.source == nil {
|
||||||
|
if id == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("document version not found: %s", id)
|
||||||
|
}
|
||||||
|
reader, err := e.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if id == "" && reader.OFD.DocBody[reader.documentIndex].DocRoot == "" {
|
||||||
|
return fmt.Errorf("document has no main entry")
|
||||||
|
}
|
||||||
|
versions, err := reader.DocumentVersionsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
found, changed := id == "", false
|
found, changed := id == "", false
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
found = found || version.ID == id
|
found = found || version.ID == id
|
||||||
changed = changed || version.Current != (version.ID == id)
|
changed = changed || version.Current != (version.ID == id)
|
||||||
}
|
}
|
||||||
@@ -229,14 +280,14 @@ func (e *Editor) SetDefaultVersion(id string) error {
|
|||||||
return fmt.Errorf("document version not found: %s", id)
|
return fmt.Errorf("document version not found: %s", id)
|
||||||
}
|
}
|
||||||
if !changed {
|
if !changed {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
return e.editDocumentSnapshot(ctx, reader, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
current, err := reader.DocumentVersionID()
|
current, err := reader.DocumentVersionID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
updated, err := editorVersionDefault(data, root.childAt("DocBody", reader.documentIndex), id)
|
updated, err := editorVersionDefault(ctx, data, root.childAt("DocBody", reader.documentIndex), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -250,7 +301,24 @@ func (e *Editor) SetDefaultVersion(id string) error {
|
|||||||
// 入参: id 版本标识, name 版本名称,空值清空名称
|
// 入参: id 版本标识, name 版本名称,空值清空名称
|
||||||
// 返回: error 修改错误
|
// 返回: error 修改错误
|
||||||
func (e *Editor) RenameVersion(id, name string) error {
|
func (e *Editor) RenameVersion(id, name string) error {
|
||||||
versions, err := e.DocumentVersions()
|
return e.RenameVersionContext(context.Background(), id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameVersionContext 修改版本名称,取消或失败时保留描述文件、共享资源和撤销历史
|
||||||
|
// 入参: ctx 取消上下文, id 版本标识, name 版本名称,空值清空名称
|
||||||
|
// 返回: error 修改或取消错误
|
||||||
|
func (e *Editor) RenameVersionContext(ctx context.Context, id, name string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.source == nil {
|
||||||
|
return fmt.Errorf("document version not found: %s", id)
|
||||||
|
}
|
||||||
|
reader, err := e.ReaderContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
versions, err := reader.DocumentVersionsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -259,15 +327,15 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
return fmt.Errorf("document version not found: %s", id)
|
return fmt.Errorf("document version not found: %s", id)
|
||||||
}
|
}
|
||||||
if versions[index].Name == name {
|
if versions[index].Name == name {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
return e.editDocumentSnapshot(ctx, reader, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
version := versions[index]
|
version := versions[index]
|
||||||
original, err := reader.readFile(version.Location)
|
original, err := reader.readFileViewContext(ctx, version.Location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
descriptor, err := parseEditorXML(original)
|
descriptor, err := parseEditorXMLContext(ctx, original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -278,6 +346,9 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
location := packageAvailableName(reader, nil, version.Location)
|
location := packageAvailableName(reader, nil, version.Location)
|
||||||
body := root.childAt("DocBody", reader.documentIndex)
|
body := root.childAt("DocBody", reader.documentIndex)
|
||||||
for _, node := range body.child("Versions").children {
|
for _, node := range body.child("Versions").children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if node.matchesOFD("Version") && node.attr("ID") == id {
|
if node.matchesOFD("Version") && node.attr("ID") == id {
|
||||||
fragment, err := editorXMLAttribute(data, node, "BaseLoc", "/"+location)
|
fragment, err := editorXMLAttribute(data, node, "BaseLoc", "/"+location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -288,7 +359,7 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.files[location] = updated
|
reader.files[location] = updated
|
||||||
if err := reader.retireVersionFiles(context.Background(), map[string]bool{version.Location: true}); err != nil {
|
if err := reader.retireVersionFiles(ctx, map[string]bool{version.Location: true}); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return reader.documentIndex, nil
|
return reader.documentIndex, nil
|
||||||
@@ -297,6 +368,7 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
|
|
||||||
// DeleteVersion 删除版本及其独占文件,可通过Undo撤销,不删除文档主入口
|
// DeleteVersion 删除版本及其独占文件,可通过Undo撤销,不删除文档主入口
|
||||||
// 删除当前版本后转到剩余默认版本或主入口;修改入口可能使已有签名失效
|
// 删除当前版本后转到剩余默认版本或主入口;修改入口可能使已有签名失效
|
||||||
|
// 无主入口时保留至少一个版本,需要替代默认版本时选择剩余版本中版本号最大的条目
|
||||||
// 入参: id 版本标识,不能为空
|
// 入参: id 版本标识,不能为空
|
||||||
// 返回: error 删除错误
|
// 返回: error 删除错误
|
||||||
func (e *Editor) DeleteVersion(id string) error {
|
func (e *Editor) DeleteVersion(id string) error {
|
||||||
@@ -321,6 +393,30 @@ func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
replacement := ""
|
||||||
|
if reader.OFD.DocBody[reader.documentIndex].DocRoot == "" {
|
||||||
|
if len(versions) == 1 {
|
||||||
|
return 0, fmt.Errorf("document must retain at least one version")
|
||||||
|
}
|
||||||
|
var fallback *DocumentVersionInfo
|
||||||
|
hasDefault := false
|
||||||
|
for i := range versions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if i == index {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
version := &versions[i]
|
||||||
|
hasDefault = hasDefault || version.Current
|
||||||
|
if fallback == nil || version.Index > fallback.Index {
|
||||||
|
fallback = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasDefault && (versions[index].Current || current == id) {
|
||||||
|
replacement = fallback.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
list := root.childAt("DocBody", reader.documentIndex).child("Versions")
|
list := root.childAt("DocBody", reader.documentIndex).child("Versions")
|
||||||
for _, node := range list.children {
|
for _, node := range list.children {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -334,6 +430,17 @@ func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if replacement != "" {
|
||||||
|
data = reader.files["OFD.xml"]
|
||||||
|
root, err = parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
reader.files["OFD.xml"], err = editorVersionDefault(ctx, data, root.childAt("DocBody", reader.documentIndex), replacement)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if current == id {
|
if current == id {
|
||||||
reader.selectedVersion = nil
|
reader.selectedVersion = nil
|
||||||
}
|
}
|
||||||
@@ -352,15 +459,21 @@ func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// editorVersionDefault 修改版本入口的Current属性,不改变条目顺序或其他属性
|
// editorVersionDefault 修改版本入口的Current属性,不改变条目顺序或其他属性
|
||||||
// 入参: data OFD入口原文, body 文档节点, id 默认版本,空值取消默认版本
|
// 入参: ctx 取消上下文, data OFD入口原文, body 文档节点, id 默认版本,空值取消默认版本
|
||||||
// 返回: []byte 更新后的入口, error 属性编码错误
|
// 返回: []byte 更新后的入口, error 属性编码错误
|
||||||
func editorVersionDefault(data []byte, body *editorXML, id string) ([]byte, error) {
|
func editorVersionDefault(ctx context.Context, data []byte, body *editorXML, id string) ([]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
list := body.child("Versions")
|
list := body.child("Versions")
|
||||||
if list == nil {
|
if list == nil {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
for _, node := range list.children {
|
for _, node := range list.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !node.matchesOFD("Version") {
|
if !node.matchesOFD("Version") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -318,7 +318,7 @@ func editorXMLSupported(node *editorXML) bool {
|
|||||||
var allowed, children string
|
var allowed, children string
|
||||||
switch node.name.Local {
|
switch node.name.Local {
|
||||||
case "TextObject", "Text":
|
case "TextObject", "Text":
|
||||||
allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "Clips FillColor StrokeColor CGTransform TextCode"
|
allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Cap DashPattern DashOffset Italic Visible Stroke Fill Alpha", "Clips FillColor StrokeColor CGTransform TextCode"
|
||||||
case "CGTransform":
|
case "CGTransform":
|
||||||
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
|
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
|
||||||
case "Glyphs":
|
case "Glyphs":
|
||||||
@@ -362,7 +362,7 @@ func editorXMLTransformable(node *editorXML) bool {
|
|||||||
var attrs string
|
var attrs string
|
||||||
switch node.name.Local {
|
switch node.name.Local {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
attrs = "ID Boundary CTM DrawParam Font Size HScale VScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Decoration Visible Stroke Fill Alpha"
|
attrs = "ID Boundary CTM DrawParam Font Size HScale VScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Cap DashPattern DashOffset Italic Decoration Visible Stroke Fill Alpha"
|
||||||
case "PathObject":
|
case "PathObject":
|
||||||
attrs = "ID Boundary CTM DrawParam LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha"
|
attrs = "ID Boundary CTM DrawParam LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha"
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
|
|||||||
+21
-6
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -87,7 +88,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
if r.imageCatalog != nil {
|
if r.imageCatalog != nil {
|
||||||
return slices.Clone(r.imageCatalog), nil
|
return slices.Clone(r.imageCatalog), nil
|
||||||
}
|
}
|
||||||
doc, err := r.Doc()
|
doc, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -103,7 +104,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
header, err := r.readPageHeader(page)
|
header, err := r.readPageHeaderContext(ctx, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -122,15 +123,19 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
data, err := r.readFile(name)
|
data, err := r.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var res Res
|
var res Res
|
||||||
if err := xml.Unmarshal(data, &res); err != nil {
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
|
if err := decoder.Decode(&res); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, media := range res.MultiMedias.MultiMedia {
|
for _, media := range res.MultiMedias.MultiMedia {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if media.Type != "Image" {
|
if media.Type != "Image" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -150,6 +155,9 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
r.imageCatalog = items
|
r.imageCatalog = items
|
||||||
return slices.Clone(items), nil
|
return slices.Clone(items), nil
|
||||||
}
|
}
|
||||||
@@ -163,15 +171,19 @@ func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
|||||||
}
|
}
|
||||||
name, _ := resourceValue(r.resourceFiles, id)
|
name, _ := resourceValue(r.resourceFiles, id)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
data, err := r.readFile(name)
|
data, err := r.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
}
|
}
|
||||||
var res Res
|
var res Res
|
||||||
if err := xml.Unmarshal(data, &res); err != nil {
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
|
if err := decoder.Decode(&res); err != nil {
|
||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
}
|
}
|
||||||
for _, media := range res.MultiMedias.MultiMedia {
|
for _, media := range res.MultiMedias.MultiMedia {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
if imageResourceKey(media.ID) == imageResourceKey(id) && media.Type == "Image" && media.MediaFile != "" {
|
if imageResourceKey(media.ID) == imageResourceKey(id) && media.Type == "Image" && media.MediaFile != "" {
|
||||||
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
||||||
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
||||||
@@ -184,6 +196,9 @@ func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
|||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
}
|
}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
if imageResourceKey(item.ID) == imageResourceKey(id) {
|
if imageResourceKey(item.ID) == imageResourceKey(id) {
|
||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ const (
|
|||||||
// GeometryOperation 指定路径填充区域的布尔运算
|
// GeometryOperation 指定路径填充区域的布尔运算
|
||||||
type GeometryOperation uint8
|
type GeometryOperation uint8
|
||||||
|
|
||||||
// StrokeOptions 描述描边参数,长度单位为毫米
|
// StrokeOptions 描述描边参数,长度单位为毫米,MiterLimitSet区分显式零值与默认截断值
|
||||||
type StrokeOptions struct {
|
type StrokeOptions struct {
|
||||||
// Width为正有限描边宽度
|
// Width为正有限描边宽度
|
||||||
Width float64
|
Width float64
|
||||||
// Cap和Join使用OFD名称,空值分别采用Butt和Miter
|
// Cap和Join使用OFD名称,空值分别采用Butt和Miter
|
||||||
Cap, Join string
|
Cap, Join string
|
||||||
// MiterLimit为尖角长度与半线宽的比值上限,0采用默认值
|
// MiterLimit为尖角长度与半线宽的比值上限
|
||||||
MiterLimit float64
|
MiterLimit float64
|
||||||
|
MiterLimitSet bool
|
||||||
// DashOffset为绝对长度的虚线相位,允许负值
|
// DashOffset为绝对长度的虚线相位,允许负值
|
||||||
DashOffset float64
|
DashOffset float64
|
||||||
// Tolerance为曲线展开误差目标,0采用后端默认精度
|
// Tolerance为曲线展开误差目标,0采用后端默认精度
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit := options.MiterLimit
|
limit := options.MiterLimit
|
||||||
if limit == 0 {
|
if limit == 0 && !options.MiterLimitSet {
|
||||||
limit = defaultMiterLimit
|
limit = defaultMiterLimit
|
||||||
}
|
}
|
||||||
if options.Join == "" || options.Join == "Miter" {
|
if options.Join == "" || options.Join == "Miter" {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// 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 (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MeshSampler 保存独立网格快照和空间索引,支持并发采样
|
||||||
|
type MeshSampler struct {
|
||||||
|
mesh MeshShading
|
||||||
|
nodes []meshSampleNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// meshSampleNode 保存连续三角形组的包围盒及右子树位置
|
||||||
|
type meshSampleNode struct {
|
||||||
|
minX, minY, maxX, maxY float64
|
||||||
|
right int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSampler 创建网格快照,后续修改原网格不影响采样结果
|
||||||
|
// 返回: *MeshSampler 只读采样器
|
||||||
|
func (m *MeshShading) NewSampler() *MeshSampler {
|
||||||
|
s := &MeshSampler{}
|
||||||
|
if m != nil {
|
||||||
|
s.mesh = *m
|
||||||
|
s.mesh.Triangles = slices.Clone(m.Triangles)
|
||||||
|
if len(m.Triangles) > 8 {
|
||||||
|
s.build(0, len(m.Triangles))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// At 采样快照中的预乘颜色,后绘制的三角形优先
|
||||||
|
// 入参: x 横坐标, y 纵坐标
|
||||||
|
// 返回: color.RGBA 预乘颜色
|
||||||
|
func (s *MeshSampler) At(x, y float64) color.RGBA {
|
||||||
|
if len(s.nodes) == 0 || !finite(x) || !finite(y) {
|
||||||
|
return s.mesh.At(x, y)
|
||||||
|
}
|
||||||
|
if value, ok := s.sample(0, 0, len(s.mesh.Triangles), x, y); ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return s.mesh.Background
|
||||||
|
}
|
||||||
|
|
||||||
|
// build 按绘制顺序构建包围盒层级,不重排原三角形
|
||||||
|
// 入参: first 起始三角形, end 结束位置
|
||||||
|
// 返回: int 节点位置
|
||||||
|
func (s *MeshSampler) build(first, end int) int {
|
||||||
|
index := len(s.nodes)
|
||||||
|
s.nodes = append(s.nodes, meshSampleNode{})
|
||||||
|
if end-first <= 8 {
|
||||||
|
node := meshSampleNode{minX: math.Inf(1), minY: math.Inf(1), maxX: math.Inf(-1), maxY: math.Inf(-1)}
|
||||||
|
for _, triangle := range s.mesh.Triangles[first:end] {
|
||||||
|
a, b, c := triangle[0].Point, triangle[1].Point, triangle[2].Point
|
||||||
|
d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
|
||||||
|
if !finite(d) {
|
||||||
|
node = meshSampleNode{minX: math.Inf(-1), minY: math.Inf(-1), maxX: math.Inf(1), maxY: math.Inf(1)}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, vertex := range triangle {
|
||||||
|
node.minX, node.minY = math.Min(node.minX, vertex.Point.X), math.Min(node.minY, vertex.Point.Y)
|
||||||
|
node.maxX, node.maxY = math.Max(node.maxX, vertex.Point.X), math.Max(node.maxY, vertex.Point.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if finite(node.minX) && finite(node.minY) && finite(node.maxX) && finite(node.maxY) {
|
||||||
|
dx := 2e-12*(node.maxX-node.minX) + 1e-14*math.Max(math.Abs(node.minX), math.Abs(node.maxX))
|
||||||
|
dy := 2e-12*(node.maxY-node.minY) + 1e-14*math.Max(math.Abs(node.minY), math.Abs(node.maxY))
|
||||||
|
node.minX, node.maxX = math.Nextafter(node.minX-dx, math.Inf(-1)), math.Nextafter(node.maxX+dx, math.Inf(1))
|
||||||
|
node.minY, node.maxY = math.Nextafter(node.minY-dy, math.Inf(-1)), math.Nextafter(node.maxY+dy, math.Inf(1))
|
||||||
|
}
|
||||||
|
s.nodes[index] = node
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
middle := first + (end-first)/2
|
||||||
|
left := s.build(first, middle)
|
||||||
|
right := s.build(middle, end)
|
||||||
|
a, b := s.nodes[left], s.nodes[right]
|
||||||
|
s.nodes[index] = meshSampleNode{minX: math.Min(a.minX, b.minX), minY: math.Min(a.minY, b.minY), maxX: math.Max(a.maxX, b.maxX), maxY: math.Max(a.maxY, b.maxY), right: right}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
// sample 优先查询后绘制的区域,包围盒外跳过整组三角形
|
||||||
|
// 入参: index 节点位置, first 起始三角形, end 结束位置, x 横坐标, y 纵坐标
|
||||||
|
// 返回: color.RGBA 采样颜色, bool 是否命中
|
||||||
|
func (s *MeshSampler) sample(index, first, end int, x, y float64) (color.RGBA, bool) {
|
||||||
|
node := s.nodes[index]
|
||||||
|
if x < node.minX || x > node.maxX || y < node.minY || y > node.maxY {
|
||||||
|
return color.RGBA{}, false
|
||||||
|
}
|
||||||
|
if end-first <= 8 {
|
||||||
|
for i := end - 1; i >= first; i-- {
|
||||||
|
if value, ok := meshTriangleColor(&s.mesh.Triangles[i], x, y); ok {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return color.RGBA{}, false
|
||||||
|
}
|
||||||
|
middle := first + (end-first)/2
|
||||||
|
if value, ok := s.sample(node.right, middle, end, x, y); ok {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
return s.sample(index+1, first, middle, x, y)
|
||||||
|
}
|
||||||
+8
-2
@@ -70,7 +70,7 @@ func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析路径并区分省略样式、显式实线与零线宽
|
// UnmarshalXML 解析路径并区分省略样式、显式实线与零值描边参数
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
@@ -87,6 +87,9 @@ func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
if attr.Name.Local == "LineWidth" {
|
if attr.Name.Local == "LineWidth" {
|
||||||
p.LineWidthSet = p.LineWidth == 0
|
p.LineWidthSet = p.LineWidth == 0
|
||||||
}
|
}
|
||||||
|
if attr.Name.Local == "MiterLimit" {
|
||||||
|
p.MiterLimitSet = p.MiterLimit == 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if value.DashPattern != nil {
|
if value.DashPattern != nil {
|
||||||
p.DashPattern = *value.DashPattern
|
p.DashPattern = *value.DashPattern
|
||||||
@@ -95,7 +98,7 @@ func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析绘制参数并保留显式实线和零线宽对基础参数的覆盖
|
// UnmarshalXML 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
@@ -112,6 +115,9 @@ func (p *DrawParam) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
if attr.Name.Local == "LineWidth" {
|
if attr.Name.Local == "LineWidth" {
|
||||||
p.LineWidthSet = p.LineWidth == 0
|
p.LineWidthSet = p.LineWidth == 0
|
||||||
}
|
}
|
||||||
|
if attr.Name.Local == "MiterLimit" {
|
||||||
|
p.MiterLimitSet = p.MiterLimit == 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if value.DashPattern != nil {
|
if value.DashPattern != nil {
|
||||||
p.DashPattern = *value.DashPattern
|
p.DashPattern = *value.DashPattern
|
||||||
|
|||||||
@@ -83,11 +83,15 @@ type ClipArea struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TextObject 文本对象
|
// TextObject 文本对象
|
||||||
|
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
|
||||||
type TextObject struct {
|
type TextObject struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
Boundary string `xml:"Boundary,attr"`
|
Boundary string `xml:"Boundary,attr"`
|
||||||
DrawParam string `xml:"DrawParam,attr"`
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
LineWidth float64 `xml:"LineWidth,attr"`
|
LineWidth float64 `xml:"LineWidth,attr"`
|
||||||
|
Cap string `xml:"Cap,attr"`
|
||||||
|
DashPattern string `xml:"DashPattern,attr"`
|
||||||
|
DashOffset *float64 `xml:"DashOffset,attr"`
|
||||||
Join string `xml:"Join,attr"`
|
Join string `xml:"Join,attr"`
|
||||||
MiterLimit float64 `xml:"MiterLimit,attr"`
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||||
Font string `xml:"Font,attr"`
|
Font string `xml:"Font,attr"`
|
||||||
@@ -111,6 +115,8 @@ type TextObject struct {
|
|||||||
Clips *Clips `xml:"Clips"`
|
Clips *Clips `xml:"Clips"`
|
||||||
Actions []Action `xml:"Actions>Action"`
|
Actions []Action `xml:"Actions>Action"`
|
||||||
LineWidthSet bool `xml:"-"`
|
LineWidthSet bool `xml:"-"`
|
||||||
|
MiterLimitSet bool `xml:"-"`
|
||||||
|
dashPatternSet bool
|
||||||
layout *textLayout
|
layout *textLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +177,7 @@ type CGTransform struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PathObject 路径对象
|
// PathObject 路径对象
|
||||||
|
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
|
||||||
type PathObject struct {
|
type PathObject struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
Boundary string `xml:"Boundary,attr"`
|
Boundary string `xml:"Boundary,attr"`
|
||||||
@@ -193,6 +200,7 @@ type PathObject struct {
|
|||||||
Clips *Clips `xml:"Clips"`
|
Clips *Clips `xml:"Clips"`
|
||||||
Actions []Action `xml:"Actions>Action"`
|
Actions []Action `xml:"Actions>Action"`
|
||||||
LineWidthSet bool `xml:"-"`
|
LineWidthSet bool `xml:"-"`
|
||||||
|
MiterLimitSet bool `xml:"-"`
|
||||||
dashPatternSet bool
|
dashPatternSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-10
@@ -63,30 +63,38 @@ type MeshShading struct {
|
|||||||
// 返回: color.RGBA 采样颜色
|
// 返回: color.RGBA 采样颜色
|
||||||
func (m *MeshShading) At(x, y float64) color.RGBA {
|
func (m *MeshShading) At(x, y float64) color.RGBA {
|
||||||
for i := len(m.Triangles) - 1; i >= 0; i-- {
|
for i := len(m.Triangles) - 1; i >= 0; i-- {
|
||||||
t := m.Triangles[i]
|
if value, ok := meshTriangleColor(&m.Triangles[i], x, y); ok {
|
||||||
a, b, c := t[0].Point, t[1].Point, t[2].Point
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.Background
|
||||||
|
}
|
||||||
|
|
||||||
|
// meshTriangleColor 按面积权重采样单个三角形,范围外不产生颜色
|
||||||
|
// 入参: triangle 三角形顶点, x 横坐标, y 纵坐标
|
||||||
|
// 返回: color.RGBA 预乘颜色, bool 是否位于三角形内
|
||||||
|
func meshTriangleColor(triangle *[3]MeshVertex, x, y float64) (color.RGBA, bool) {
|
||||||
|
a, b, c := triangle[0].Point, triangle[1].Point, triangle[2].Point
|
||||||
d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
|
d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y)
|
||||||
if d == 0 {
|
if d == 0 {
|
||||||
continue
|
return color.RGBA{}, false
|
||||||
}
|
}
|
||||||
u := ((b.Y-c.Y)*(x-c.X) + (c.X-b.X)*(y-c.Y)) / d
|
u := ((b.Y-c.Y)*(x-c.X) + (c.X-b.X)*(y-c.Y)) / d
|
||||||
v := ((c.Y-a.Y)*(x-c.X) + (a.X-c.X)*(y-c.Y)) / d
|
v := ((c.Y-a.Y)*(x-c.X) + (a.X-c.X)*(y-c.Y)) / d
|
||||||
w := 1 - u - v
|
w := 1 - u - v
|
||||||
if u < -1e-12 || v < -1e-12 || w < -1e-12 {
|
if u < -1e-12 || v < -1e-12 || w < -1e-12 {
|
||||||
continue
|
return color.RGBA{}, false
|
||||||
}
|
}
|
||||||
var values [4]float64
|
var values [4]float64
|
||||||
alpha := 0.0
|
alpha := 0.0
|
||||||
for j, weight := range [3]float64{u, v, w} {
|
for j, weight := range [3]float64{u, v, w} {
|
||||||
alpha += weight * t[j].Alpha
|
alpha += weight * triangle[j].Alpha
|
||||||
for k := range values {
|
for k := range values {
|
||||||
values[k] += weight * t[j].Values[k]
|
values[k] += weight * triangle[j].Values[k]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
red, green, blue := t[0].rgb(values)
|
red, green, blue := triangle[0].rgb(values)
|
||||||
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}
|
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}, true
|
||||||
}
|
|
||||||
return m.Background
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用
|
// outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用
|
||||||
|
|||||||
+27
-8
@@ -381,6 +381,23 @@ func pdfNumbers(values ...float64) string {
|
|||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfDashPattern 换算PDF虚线长度,将单值数组展开为等长线段与间隔
|
||||||
|
// 入参: dash PDF虚线数组, scale 坐标缩放比例
|
||||||
|
// 返回: string OFD虚线数组
|
||||||
|
func pdfDashPattern(dash []float64, scale float64) string {
|
||||||
|
if len(dash) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
values := make([]float64, max(2, len(dash)))
|
||||||
|
for i, value := range dash {
|
||||||
|
values[i] = value * scale
|
||||||
|
}
|
||||||
|
if len(dash) == 1 {
|
||||||
|
values[1] = values[0]
|
||||||
|
}
|
||||||
|
return pdfNumbers(values...)
|
||||||
|
}
|
||||||
|
|
||||||
// pdfBoundary 序列化对象边界
|
// pdfBoundary 序列化对象边界
|
||||||
// 入参: b 对象边界
|
// 入参: b 对象边界
|
||||||
// 返回: string OFD边界数据
|
// 返回: string OFD边界数据
|
||||||
@@ -618,7 +635,10 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
||||||
}
|
}
|
||||||
if p.warning != nil && (mark.Fill && pdfGradientError(mark.Style.Fill) != nil || mark.Stroke && pdfGradientError(mark.Style.Stroke) != nil) {
|
if p.warning != nil && (mark.Fill && p.gradientError(mark.Style.Fill) != nil || mark.Stroke && p.gradientError(mark.Style.Stroke) != nil) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return p.gradientPath(mark)
|
return p.gradientPath(mark)
|
||||||
}
|
}
|
||||||
box, err := p.pathBoundsContext(p.ctx, mark.Path)
|
box, err := p.pathBoundsContext(p.ctx, mark.Path)
|
||||||
@@ -652,7 +672,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
ctm = pdfNumbers(m[:]...)
|
ctm = pdfNumbers(m[:]...)
|
||||||
}
|
}
|
||||||
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
||||||
if value.Tiling != nil {
|
if value.Tiling != nil || pdfGradientPattern(value) {
|
||||||
return p.paintColor(value, box)
|
return p.paintColor(value, box)
|
||||||
}
|
}
|
||||||
return drawing.paintColor(value, origin)
|
return drawing.paintColor(value, origin)
|
||||||
@@ -685,11 +705,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
object.Rule = "Even-Odd"
|
object.Rule = "Even-Odd"
|
||||||
}
|
}
|
||||||
if len(mark.Style.Dash) > 0 {
|
if len(mark.Style.Dash) > 0 {
|
||||||
values := make([]float64, len(mark.Style.Dash))
|
object.DashPattern = pdfDashPattern(mark.Style.Dash, scale)
|
||||||
for n, v := range mark.Style.Dash {
|
|
||||||
values[n] = v * scale
|
|
||||||
}
|
|
||||||
object.DashPattern = pdfNumbers(values...)
|
|
||||||
phase := mark.Style.DashPhase * scale
|
phase := mark.Style.DashPhase * scale
|
||||||
object.DashOffset = &phase
|
object.DashOffset = &phase
|
||||||
}
|
}
|
||||||
@@ -702,9 +718,12 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
// 入参: paint PDF画刷, box 对象边界
|
// 入参: paint PDF画刷, box 对象边界
|
||||||
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误
|
||||||
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||||
if err := pdfGradientError(paint); err != nil {
|
if err := p.gradientError(paint); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if pdfGradientPattern(paint) {
|
||||||
|
return p.gradientPattern(paint, box)
|
||||||
|
}
|
||||||
if paint.Mesh != nil {
|
if paint.Mesh != nil {
|
||||||
return p.meshColor(paint, box)
|
return p.meshColor(paint, box)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -114,6 +114,7 @@ type pdfCompositeCache struct {
|
|||||||
imageProcesses map[pdfImageProcessKey]*pdfgo.ImageProcess
|
imageProcesses map[pdfImageProcessKey]*pdfgo.ImageProcess
|
||||||
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||||
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
||||||
|
linearMeshes *renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]
|
||||||
patterns map[pdfgo.Paint]*pdfCompositePattern
|
patterns map[pdfgo.Paint]*pdfCompositePattern
|
||||||
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
||||||
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
|
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
|
||||||
@@ -637,16 +638,19 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// opaque 检查图元是否无需与页面背景混合
|
// opaque 检查图元是否无需与页面背景混合
|
||||||
// 入参: space 当前混合空间
|
// 入参: n 待检查图元, space 当前混合空间
|
||||||
// 返回: bool 是否可直接保留, error 遮罩解析错误
|
// 返回: bool 是否可直接保留, error 遮罩解析或取消错误
|
||||||
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) (bool, error) {
|
func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool, error) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
if n.group != nil {
|
if n.group != nil {
|
||||||
g := n.group
|
g := n.group
|
||||||
if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
|
if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
for _, child := range n.children {
|
for _, child := range n.children {
|
||||||
if opaque, err := child.opaque(space); err != nil || !opaque {
|
if opaque, err := p.opaque(child, space); err != nil || !opaque {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,8 +666,8 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) (bool, error) {
|
|||||||
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
if fill && pdfGradientError(style.Fill) != nil || stroke && pdfGradientError(style.Stroke) != nil {
|
if fill && p.gradientError(style.Fill) != nil || stroke && p.gradientError(style.Stroke) != nil {
|
||||||
return false, nil
|
return false, p.ctx.Err()
|
||||||
}
|
}
|
||||||
if n.image != nil {
|
if n.image != nil {
|
||||||
masked, err := n.image.Image.HasMask()
|
masked, err := n.image.Image.HasMask()
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.Co
|
|||||||
if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && pdfNormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) {
|
if group := node.group; group != nil && !group.Knockout && group.Alpha == 1 && group.SoftMask == nil && pdfNormalBlend(group.BlendMode) && (group.ColorSpace == nil || group.ColorSpace.SRGBEquivalent()) {
|
||||||
directGroup = true
|
directGroup = true
|
||||||
}
|
}
|
||||||
opaque, err := node.opaque(space)
|
opaque, err := p.opaque(node, space)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-5
@@ -18,6 +18,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -49,11 +50,58 @@ type pdfMeshSample struct {
|
|||||||
patch int
|
patch int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfLinearMeshKey 定位源网格中需分段展开的三角形,不包含输出变换
|
||||||
|
type pdfLinearMeshKey struct {
|
||||||
|
mesh *pdfgo.MeshGradient
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// linearMeshTriangles 在单页内复用只读分区,固定布局计量内存并丢弃超预算缓存
|
||||||
|
// 入参: mesh 源网格, index 源三角形索引
|
||||||
|
// 返回: []pdfgo.MeshTriangle 只读分区结果, error 转换或取消错误
|
||||||
|
func (p *pdfImporter) linearMeshTriangles(mesh *pdfgo.MeshGradient, index int) ([]pdfgo.MeshTriangle, error) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cache := p.compositingCache()
|
||||||
|
if cache.linearMeshes == nil {
|
||||||
|
cache.linearMeshes = &renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]{limit: 8 << 20}
|
||||||
|
}
|
||||||
|
key := pdfLinearMeshKey{mesh: mesh, index: index}
|
||||||
|
if triangles, ok := cache.linearMeshes.get(key); ok {
|
||||||
|
return triangles, nil
|
||||||
|
}
|
||||||
|
triangles, err := mesh.LinearTrianglesContext(p.ctx, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(triangles) > (cache.linearMeshes.limit-64)/216 {
|
||||||
|
return triangles, nil
|
||||||
|
}
|
||||||
|
stored := make([]pdfgo.MeshTriangle, len(triangles))
|
||||||
|
colors := make([][3][4]float64, len(triangles))
|
||||||
|
for i, triangle := range triangles {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stored[i].Points = triangle.Points
|
||||||
|
for j, values := range triangle.Colors {
|
||||||
|
copy(colors[i][j][:], values)
|
||||||
|
stored[i].Colors[j] = colors[i][j][:len(values):len(values)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cache.linearMeshes.put(key, stored, 64+len(stored)*216)
|
||||||
|
return stored, nil
|
||||||
|
}
|
||||||
|
|
||||||
// meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序
|
// meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序
|
||||||
// 入参: paint PDF网格画刷, box 对象边界
|
// 入参: paint PDF网格画刷, box 对象边界
|
||||||
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
|
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
|
||||||
func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||||
if err := pdfGradientError(paint); err != nil {
|
if err := p.gradientError(paint); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mesh := paint.Mesh
|
mesh := paint.Mesh
|
||||||
@@ -67,11 +115,43 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, triangle := range mesh.Triangles {
|
var previous pdfgo.MeshTriangle
|
||||||
|
var previousColors [3][4]float64
|
||||||
|
var colors [3][4]float64
|
||||||
|
for i := range previous.Colors {
|
||||||
|
previous.Colors[i] = previousColors[i][:mesh.Space.Components()]
|
||||||
|
}
|
||||||
|
for index, triangle := range mesh.Triangles {
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i, point := range triangle.Points {
|
var err error
|
||||||
|
colors, err = mesh.LinearColors(index)
|
||||||
|
var triangles []pdfgo.MeshTriangle
|
||||||
|
if err != nil {
|
||||||
|
triangles, err = p.linearMeshTriangles(mesh, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := range triangle.Colors {
|
||||||
|
triangle.Colors[i] = colors[i][:mesh.Space.Components()]
|
||||||
|
}
|
||||||
|
triangles = []pdfgo.MeshTriangle{triangle}
|
||||||
|
}
|
||||||
|
for _, triangle := range triangles {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
first, flag := 0, 0
|
||||||
|
if len(shading.Point) > 0 {
|
||||||
|
flag = pdfMeshEdge(previous, triangle)
|
||||||
|
if flag != 0 {
|
||||||
|
first = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := first; i < len(triangle.Points); i++ {
|
||||||
|
point := triangle.Points[i]
|
||||||
point = matrix.Apply(point)
|
point = matrix.Apply(point)
|
||||||
if !finite(point.X) || !finite(point.Y) {
|
if !finite(point.X) || !finite(point.Y) {
|
||||||
return nil, fmt.Errorf("nonfinite mesh vertex")
|
return nil, fmt.Errorf("nonfinite mesh vertex")
|
||||||
@@ -82,7 +162,7 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, Color: ShdColor{Value: value, ColorSpace: icc}})
|
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: value, ColorSpace: icc}})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rgb, err := mesh.Space.RGB(values[:mesh.Space.Components()], mesh.Intent)
|
rgb, err := mesh.Space.RGB(values[:mesh.Space.Components()], mesh.Intent)
|
||||||
@@ -94,8 +174,20 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
color.CMYK = (*[4]float64)(values)
|
color.CMYK = (*[4]float64)(values)
|
||||||
}
|
}
|
||||||
converted := p.color(color)
|
converted := p.color(color)
|
||||||
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}})
|
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}})
|
||||||
}
|
}
|
||||||
|
previous.Points = triangle.Points
|
||||||
|
for i := range previous.Colors {
|
||||||
|
copy(previous.Colors[i], triangle.Colors[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mesh.Background != nil {
|
||||||
|
background, err := p.gradientBackgroundColor(*mesh.Background, mesh.Space, mesh.Intent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shading.Extend, shading.BackColor = 1, &background
|
||||||
}
|
}
|
||||||
color := p.color(paint)
|
color := p.color(paint)
|
||||||
color.Value = ""
|
color.Value = ""
|
||||||
@@ -103,6 +195,21 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
|||||||
return color, nil
|
return color, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfMeshEdge 按坐标及颜色分量匹配相邻三角形的共享边,不改变顶点顺序
|
||||||
|
// 入参: previous 前一三角形, current 当前三角形
|
||||||
|
// 返回: int OFD共享边标志,零表示独立三角形
|
||||||
|
func pdfMeshEdge(previous, current pdfgo.MeshTriangle) int {
|
||||||
|
if previous.Points[2] != current.Points[1] || !slices.Equal(previous.Colors[2], current.Colors[1]) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, index := range [2]int{1, 0} {
|
||||||
|
if previous.Points[index] == current.Points[0] && slices.Equal(previous.Colors[index], current.Colors[0]) {
|
||||||
|
return 2 - index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
// meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
||||||
// 入参: mesh 原始曲面
|
// 入参: mesh 原始曲面
|
||||||
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
|
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
|
||||||
|
|||||||
+9
-2
@@ -311,7 +311,10 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
||||||
}
|
}
|
||||||
if p.warning != nil && ((paintMode == 0 || paintMode == 2) && pdfGradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && pdfGradientError(mark.Style.Stroke) != nil) {
|
if p.warning != nil && ((paintMode == 0 || paintMode == 2) && p.gradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && p.gradientError(mark.Style.Stroke) != nil) {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
gradient := mark
|
gradient := mark
|
||||||
return p.compositeRegion(nil, pdfCompositeNode{text: &gradient}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
return p.compositeRegion(nil, pdfCompositeNode{text: &gradient}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
||||||
}
|
}
|
||||||
@@ -544,10 +547,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
box = Box{box.X - margin, box.Y - margin, box.W + 2*margin, box.H + 2*margin}
|
||||||
object.LineWidth = lineWidth / scale
|
object.LineWidth = lineWidth / scale
|
||||||
object.LineWidthSet = object.LineWidth == 0
|
object.LineWidthSet = object.LineWidth == 0
|
||||||
|
object.Cap = []string{"Butt", "Round", "Square"}[mark.Style.Cap]
|
||||||
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
|
||||||
object.MiterLimit = mark.Style.MiterLimit
|
object.MiterLimit = mark.Style.MiterLimit
|
||||||
if len(mark.Style.Dash) > 0 {
|
if len(mark.Style.Dash) > 0 {
|
||||||
return &pdfgo.UnsupportedError{Feature: "dashed text stroke"}
|
strokeScale := math.Hypot(p.matrix[0], p.matrix[1]) * sx / scale
|
||||||
|
object.DashPattern = pdfDashPattern(mark.Style.Dash, strokeScale)
|
||||||
|
phase := mark.Style.DashPhase * strokeScale
|
||||||
|
object.DashOffset = &phase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
object.Boundary = pdfBoundary(box)
|
object.Boundary = pdfBoundary(box)
|
||||||
|
|||||||
+241
-10
@@ -180,10 +180,13 @@ func pdfGradientStopsError(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdfGradientError 检查渐变函数与源空间能否原生映射为OFD分段
|
// gradientError 检查渐变函数与源空间能否原生映射为OFD分段
|
||||||
// 入参: paint 画刷
|
// 入参: paint 画刷
|
||||||
// 返回: error 不可等价表达的渐变
|
// 返回: error 不可等价表达的渐变或取消错误
|
||||||
func pdfGradientError(paint pdfgo.Paint) error {
|
func (p *pdfImporter) gradientError(paint pdfgo.Paint) error {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if paint.Shading != nil {
|
if paint.Shading != nil {
|
||||||
return &pdfgo.UnsupportedError{Feature: "shading pattern state compositing"}
|
return &pdfgo.UnsupportedError{Feature: "shading pattern state compositing"}
|
||||||
}
|
}
|
||||||
@@ -192,26 +195,254 @@ func pdfGradientError(paint pdfgo.Paint) error {
|
|||||||
}
|
}
|
||||||
if paint.Mesh != nil {
|
if paint.Mesh != nil {
|
||||||
mesh := paint.Mesh
|
mesh := paint.Mesh
|
||||||
if len(mesh.Patches) != 0 || len(mesh.Triangles) == 0 || mesh.UsesFunction() || mesh.Background != nil || mesh.Bounds != nil {
|
if len(mesh.Patches) != 0 || len(mesh.Triangles) == 0 {
|
||||||
return &pdfgo.UnsupportedError{Feature: "nonlinear mesh conversion"}
|
return &pdfgo.UnsupportedError{Feature: "nonlinear mesh conversion"}
|
||||||
}
|
}
|
||||||
|
for i := range mesh.Triangles {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := mesh.LinearColors(i); err != nil {
|
||||||
|
if _, err := p.linearMeshTriangles(mesh, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if mesh.Space.Calibrated() && !pdfICCTransfer(mesh.Space) && !mesh.Space.SRGBEquivalent() {
|
if mesh.Space.Calibrated() && !pdfICCTransfer(mesh.Space) && !mesh.Space.SRGBEquivalent() {
|
||||||
return &pdfgo.UnsupportedError{Feature: "nonlinear calibrated mesh conversion"}
|
return &pdfgo.UnsupportedError{Feature: "nonlinear calibrated mesh conversion"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if paint.Axial != nil {
|
if paint.Axial != nil {
|
||||||
if paint.Axial.Background != nil || paint.Axial.Bounds != nil {
|
|
||||||
return &pdfgo.UnsupportedError{Feature: "bounded axial pattern conversion"}
|
|
||||||
}
|
|
||||||
return pdfGradientStopsError(paint.Axial.Stops, paint.Axial.Space)
|
return pdfGradientStopsError(paint.Axial.Stops, paint.Axial.Space)
|
||||||
}
|
}
|
||||||
if paint.Radial != nil {
|
if paint.Radial != nil {
|
||||||
if paint.Radial.Background != nil || paint.Radial.Bounds != nil {
|
if paint.Radial.Background != nil && !pdfRadialNested(paint.Radial) {
|
||||||
return &pdfgo.UnsupportedError{Feature: "bounded radial pattern conversion"}
|
return &pdfgo.UnsupportedError{Feature: "radial pattern background conversion"}
|
||||||
}
|
}
|
||||||
return pdfGradientStopsError(paint.Radial.Stops, paint.Radial.Space)
|
return pdfGradientStopsError(paint.Radial.Stops, paint.Radial.Space)
|
||||||
}
|
}
|
||||||
return nil
|
return p.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfGradientPattern 判断渐变是否需要底纹保留边界或背景
|
||||||
|
// 入参: paint PDF画刷
|
||||||
|
// 返回: bool 是否需要底纹单元
|
||||||
|
func pdfGradientPattern(paint pdfgo.Paint) bool {
|
||||||
|
return paint.Axial != nil && (paint.Axial.Bounds != nil || paint.Axial.Background != nil && paint.Axial.Extend != [2]bool{true, true}) || paint.Radial != nil && (paint.Radial.Bounds != nil || paint.Radial.Background != nil && paint.Radial.Extend != [2]bool{true, true}) || paint.Mesh != nil && paint.Mesh.Bounds != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfRadialNested 判断起止圆是否严格内含,确保绘制区由内外圆边界唯一限定
|
||||||
|
// 入参: gradient 径向渐变
|
||||||
|
// 返回: bool 是否严格内含
|
||||||
|
func pdfRadialNested(gradient *pdfgo.RadialGradient) bool {
|
||||||
|
return math.Hypot(gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y) < math.Abs(gradient.EndRadius-gradient.StartRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gradientPattern 用底纹单元保留渐变边界及背景,单元覆盖整个对象以避免重复绘制
|
||||||
|
// 入参: paint PDF渐变画刷, box 页面坐标中的对象边界
|
||||||
|
// 返回: *FillColor OFD底纹颜色, error 坐标或渐变转换错误
|
||||||
|
func (p *pdfImporter) gradientPattern(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||||
|
if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || box.W <= 0 || box.H <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid PDF gradient paint bounds")
|
||||||
|
}
|
||||||
|
var bounds *pdfgo.Rectangle
|
||||||
|
var background *[4]float64
|
||||||
|
var space *pdfgo.ColorSpace
|
||||||
|
var intent pdfgo.Name
|
||||||
|
var extend [2]bool
|
||||||
|
matrix := pdfgo.Identity()
|
||||||
|
plain := paint
|
||||||
|
plain.Alpha = 1
|
||||||
|
if paint.Axial != nil {
|
||||||
|
gradient := *paint.Axial
|
||||||
|
bounds, matrix = gradient.Bounds, gradient.Matrix
|
||||||
|
background, space, intent, extend = gradient.Background, gradient.Space, gradient.Intent, gradient.Extend
|
||||||
|
gradient.Bounds = nil
|
||||||
|
gradient.Background = nil
|
||||||
|
plain.Axial = &gradient
|
||||||
|
} else if paint.Radial != nil {
|
||||||
|
gradient := *paint.Radial
|
||||||
|
bounds, matrix = gradient.Bounds, gradient.Matrix
|
||||||
|
background, space, intent, extend = gradient.Background, gradient.Space, gradient.Intent, gradient.Extend
|
||||||
|
gradient.Bounds = nil
|
||||||
|
gradient.Background = nil
|
||||||
|
plain.Radial = &gradient
|
||||||
|
} else {
|
||||||
|
gradient := *paint.Mesh
|
||||||
|
bounds, matrix = gradient.Bounds, gradient.Matrix
|
||||||
|
gradient.Bounds = nil
|
||||||
|
plain.Mesh = &gradient
|
||||||
|
}
|
||||||
|
if matrix == (pdfgo.Matrix{}) {
|
||||||
|
matrix = pdfgo.Identity()
|
||||||
|
}
|
||||||
|
matrix = p.matrix.Mul(matrix)
|
||||||
|
if _, ok := matrix.Inverse(); !ok {
|
||||||
|
return nil, fmt.Errorf("invalid PDF gradient bounds matrix")
|
||||||
|
}
|
||||||
|
fill, err := p.paintColor(plain, box)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filled, stroked := true, false
|
||||||
|
boundary := pdfBoundary(Box{W: box.W, H: box.H})
|
||||||
|
object := PathObject{Boundary: boundary, AbbreviatedData: "M 0 0 L " + pdfNumbers(box.W, 0) + " L " + pdfNumbers(box.W, box.H) + " L " + pdfNumbers(0, box.H) + " C", Fill: &filled, Stroke: &stroked, FillColor: fill}
|
||||||
|
if bounds != nil {
|
||||||
|
points := [4]pdfgo.Point{{X: bounds.XMin, Y: bounds.YMin}, {X: bounds.XMax, Y: bounds.YMin}, {X: bounds.XMax, Y: bounds.YMax}, {X: bounds.XMin, Y: bounds.YMax}}
|
||||||
|
data := ""
|
||||||
|
for i, point := range points {
|
||||||
|
point = matrix.Apply(point)
|
||||||
|
if !finite(point.X) || !finite(point.Y) {
|
||||||
|
return nil, fmt.Errorf("invalid PDF gradient bounds")
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
data = "M "
|
||||||
|
} else {
|
||||||
|
data += " L "
|
||||||
|
}
|
||||||
|
data += pdfNumbers(point.X-box.X, point.Y-box.Y)
|
||||||
|
}
|
||||||
|
clip := PathObject{Boundary: boundary, AbbreviatedData: data + " C", Fill: &filled, Stroke: &stroked}
|
||||||
|
object.Clips = &Clips{Clip: []Clip{{Area: []ClipArea{{Path: []PathObject{clip}}}}}}
|
||||||
|
}
|
||||||
|
objects := []PathObject{object}
|
||||||
|
if background != nil && extend != [2]bool{true, true} {
|
||||||
|
data := ""
|
||||||
|
if paint.Axial != nil {
|
||||||
|
data, err = pdfAxialBackground(fill.AxialShd, box)
|
||||||
|
} else {
|
||||||
|
data, err = pdfRadialBackground(paint.Radial, matrix, box)
|
||||||
|
clip := object
|
||||||
|
clip.FillColor, clip.Clips = nil, nil
|
||||||
|
clips := &Clips{Clip: []Clip{{Area: []ClipArea{{Path: []PathObject{clip}}}}}}
|
||||||
|
if object.Clips != nil {
|
||||||
|
clips.Clip = append(clips.Clip, object.Clips.Clip...)
|
||||||
|
}
|
||||||
|
object.Clips = clips
|
||||||
|
object.Rule = "Even-Odd"
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data != "" {
|
||||||
|
color, err := p.gradientBackgroundColor(*background, space, intent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
object.AbbreviatedData = data
|
||||||
|
object.FillColor = &FillColor{Value: color.Value, ColorSpace: color.ColorSpace}
|
||||||
|
objects = append(objects, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
color := p.color(paint)
|
||||||
|
color.Pattern = &Pattern{Width: box.W, Height: box.H, RelativeTo: "Page", CTM: pdfNumbers(1, 0, 0, 1, box.X, box.Y), CellContent: PatternContent{PathObject: objects}}
|
||||||
|
return color, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gradientBackgroundColor 保留渐变背景的源颜色分量并复用颜色空间资源
|
||||||
|
// 入参: values 背景分量, space 源颜色空间, intent 渲染意图
|
||||||
|
// 返回: ShdColor OFD基本颜色, error 颜色转换错误
|
||||||
|
func (p *pdfImporter) gradientBackgroundColor(values [4]float64, space *pdfgo.ColorSpace, intent pdfgo.Name) (ShdColor, error) {
|
||||||
|
if space == nil || space.Components() < 1 || space.Components() > 4 {
|
||||||
|
return ShdColor{}, fmt.Errorf("invalid PDF gradient background color space")
|
||||||
|
}
|
||||||
|
stop := pdfgo.GradientStop{Values: values}
|
||||||
|
if !pdfICCTransfer(space) && (space.Model != "DeviceCMYK" || space.Calibrated()) {
|
||||||
|
var err error
|
||||||
|
stop.RGB, err = space.RGB(values[:space.Components()], intent)
|
||||||
|
if err != nil {
|
||||||
|
return ShdColor{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments, err := p.gradientSegments([]pdfgo.GradientStop{stop}, space, intent)
|
||||||
|
if err != nil {
|
||||||
|
return ShdColor{}, err
|
||||||
|
}
|
||||||
|
return segments[0].Color, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfRadialBackground 用圆弧保留内含双圆的背景区域,不离散化渐变边界
|
||||||
|
// 入参: gradient 源渐变, matrix 渐变到页面的矩阵, box 对象边界
|
||||||
|
// 返回: string 奇偶填充背景路径, error 几何转换错误
|
||||||
|
func pdfRadialBackground(gradient *pdfgo.RadialGradient, matrix pdfgo.Matrix, box Box) (string, error) {
|
||||||
|
inner, outer := gradient.Start, gradient.End
|
||||||
|
r0, r1, extend := gradient.StartRadius, gradient.EndRadius, gradient.Extend
|
||||||
|
if r1 < r0 {
|
||||||
|
inner, outer, r0, r1 = outer, inner, r1, r0
|
||||||
|
extend = [2]bool{extend[1], extend[0]}
|
||||||
|
}
|
||||||
|
var circles, path GeometryPath
|
||||||
|
if !extend[1] {
|
||||||
|
path = geometryRectangle(Box{W: box.W, H: box.H})
|
||||||
|
circles = geometryCircle(RasterPoint{X: outer.X, Y: outer.Y}, r1)
|
||||||
|
}
|
||||||
|
if !extend[0] && r0 > 0 {
|
||||||
|
circles = append(circles, geometryCircle(RasterPoint{X: inner.X, Y: inner.Y}, r0)...)
|
||||||
|
}
|
||||||
|
local := Matrix{a: matrix[0], b: matrix[1], c: matrix[2], d: matrix[3], e: matrix[4] - box.X, f: matrix[5] - box.Y}
|
||||||
|
circles, err := circles.Transform(local)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return append(path, circles...).OFD()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfAxialBackground 裁出轴向渐变未延伸的两侧区域,与渐变绘制区互不重叠
|
||||||
|
// 入参: shading 对象局部渐变, box 对象边界
|
||||||
|
// 返回: string 背景路径, error 非有限坐标
|
||||||
|
func pdfAxialBackground(shading *AxialShd, box Box) (string, error) {
|
||||||
|
start, end := parseFloats(shading.StartPoint), parseFloats(shading.EndPoint)
|
||||||
|
dx, dy := end[0]-start[0], end[1]-start[1]
|
||||||
|
scale := math.Max(math.Abs(dx), math.Abs(dy))
|
||||||
|
if !finite(scale) || scale == 0 {
|
||||||
|
return "", fmt.Errorf("invalid PDF gradient background axis")
|
||||||
|
}
|
||||||
|
dx, dy = dx/scale, dy/scale
|
||||||
|
corners := [4]pdfgo.Point{{}, {X: box.W}, {X: box.W, Y: box.H}, {Y: box.H}}
|
||||||
|
data := ""
|
||||||
|
for side, origin := range [][]float64{start, end} {
|
||||||
|
if side == 0 && (shading.Extend == "1" || shading.Extend == "3") || side == 1 && (shading.Extend == "2" || shading.Extend == "3") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
distances := [4]float64{}
|
||||||
|
for i, point := range corners {
|
||||||
|
distances[i] = (point.X-origin[0])*dx + (point.Y-origin[1])*dy
|
||||||
|
if side == 1 {
|
||||||
|
distances[i] = -distances[i]
|
||||||
|
}
|
||||||
|
if !finite(distances[i]) {
|
||||||
|
return "", fmt.Errorf("invalid PDF gradient background extent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points := make([]pdfgo.Point, 0, 5)
|
||||||
|
previous := 3
|
||||||
|
for i, point := range corners {
|
||||||
|
a, b := distances[previous], distances[i]
|
||||||
|
if (a <= 0) != (b <= 0) {
|
||||||
|
scale := math.Max(math.Abs(a), math.Abs(b))
|
||||||
|
ratio := (a / scale) / (a/scale - b/scale)
|
||||||
|
from := corners[previous]
|
||||||
|
points = append(points, pdfgo.Point{X: from.X + (point.X-from.X)*ratio, Y: from.Y + (point.Y-from.Y)*ratio})
|
||||||
|
}
|
||||||
|
if b <= 0 {
|
||||||
|
points = append(points, point)
|
||||||
|
}
|
||||||
|
previous = i
|
||||||
|
}
|
||||||
|
if len(points) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, point := range points {
|
||||||
|
if i == 0 {
|
||||||
|
data += " M "
|
||||||
|
} else {
|
||||||
|
data += " L "
|
||||||
|
}
|
||||||
|
data += pdfNumbers(point.X, point.Y)
|
||||||
|
}
|
||||||
|
data += " C"
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// gradientPath 按原始函数合成渐变路径,保留填充描边的单一对象语义
|
// gradientPath 按原始函数合成渐变路径,保留填充描边的单一对象语义
|
||||||
|
|||||||
@@ -168,7 +168,10 @@ func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) erro
|
|||||||
}
|
}
|
||||||
childBlocked := blocked
|
childBlocked := blocked
|
||||||
if group := node.group; group != nil && node.textObject == nil {
|
if group := node.group; group != nil && node.textObject == nil {
|
||||||
opaque, err := node.opaque(group.ColorSpace)
|
opaque, err := r.importer.opaque(node, group.ColorSpace)
|
||||||
|
if err := r.importer.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent()
|
childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent()
|
||||||
}
|
}
|
||||||
if err := r.count(node.children, childBlocked); err != nil {
|
if err := r.count(node.children, childBlocked); err != nil {
|
||||||
|
|||||||
+147
-26
@@ -139,14 +139,25 @@ func (r *Reader) DocumentPageCountContext(ctx context.Context, index int) (int,
|
|||||||
// 入参: index 文档索引,从0开始
|
// 入参: index 文档索引,从0开始
|
||||||
// 返回: *Reader 文档阅读器, error 错误信息
|
// 返回: *Reader 文档阅读器, error 错误信息
|
||||||
func (r *Reader) Document(index int) (*Reader, error) {
|
func (r *Reader) Document(index int) (*Reader, error) {
|
||||||
|
return r.DocumentContext(context.Background(), index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentContext 创建指定文档的独立阅读器,读取结构和关联内容时响应取消
|
||||||
|
// 返回值不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
||||||
|
// 入参: ctx 取消上下文, index 文档索引,从0开始
|
||||||
|
// 返回: *Reader 文档阅读器, error 读取或取消错误
|
||||||
|
func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if index < 0 || index >= r.DocumentCount() {
|
if index < 0 || index >= r.DocumentCount() {
|
||||||
return nil, fmt.Errorf("document index %d out of range", index)
|
return nil, fmt.Errorf("document index %d out of range", index)
|
||||||
}
|
}
|
||||||
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
|
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
|
||||||
if err := next.initRoot(); err != nil {
|
if err := next.initRootContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := next.Doc(); err != nil {
|
if _, err := next.DocContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return next, nil
|
return next, nil
|
||||||
@@ -155,15 +166,65 @@ func (r *Reader) Document(index int) (*Reader, error) {
|
|||||||
// Doc 获取当前文档结构
|
// Doc 获取当前文档结构
|
||||||
// 返回: *Document 文档结构, error 错误信息
|
// 返回: *Document 文档结构, error 错误信息
|
||||||
func (r *Reader) Doc() (*Document, error) {
|
func (r *Reader) Doc() (*Document, error) {
|
||||||
doc, err := r.docStructure()
|
return r.DocContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocContext 获取当前文档结构及关联内容,取消或失败时保留原阅读状态
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Document 文档结构, error 读取或取消错误
|
||||||
|
func (r *Reader) DocContext(ctx context.Context) (*Document, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.doc != nil && !r.relatedPending {
|
||||||
|
return r.doc, nil
|
||||||
|
}
|
||||||
|
view := *r
|
||||||
|
if r.doc == nil {
|
||||||
|
view.resourcesRead = maps.Clone(r.resourcesRead)
|
||||||
|
view.resourceFiles = maps.Clone(r.resourceFiles)
|
||||||
|
view.ResMap = maps.Clone(r.ResMap)
|
||||||
|
view.mediaCache = maps.Clone(r.mediaCache)
|
||||||
|
view.fontCache = maps.Clone(r.fontCache)
|
||||||
|
view.colorSpaceCache = maps.Clone(r.colorSpaceCache)
|
||||||
|
view.drawParamCache = maps.Clone(r.drawParamCache)
|
||||||
|
view.compositeGraphicUnitCache = maps.Clone(r.compositeGraphicUnitCache)
|
||||||
|
}
|
||||||
|
doc, err := view.docStructureContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if r.relatedPending {
|
if view.relatedPending {
|
||||||
r.relatedPending = false
|
view.Annots = maps.Clone(r.Annots)
|
||||||
_ = r.parseAnnotations(doc)
|
for page, entries := range view.Annots {
|
||||||
_ = r.parseSignatures(doc)
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
view.Annots[page] = slices.Clone(entries)
|
||||||
|
}
|
||||||
|
view.Stamps = maps.Clone(r.Stamps)
|
||||||
|
for page, entries := range view.Stamps {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
view.Stamps[page] = slices.Clone(entries)
|
||||||
|
}
|
||||||
|
_ = view.parseAnnotations(ctx, doc)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = view.parseSignatures(ctx, doc)
|
||||||
|
view.relatedPending = false
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.doc == nil {
|
||||||
|
r.commitDocumentStructure(&view)
|
||||||
|
} else {
|
||||||
|
r.relatedPending = view.relatedPending
|
||||||
|
}
|
||||||
|
r.Annots, r.Stamps, r.annotationFiles = view.Annots, view.Stamps, view.annotationFiles
|
||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,9 +263,7 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
|
|||||||
if err := decoder.Decode(&doc); err != nil {
|
if err := decoder.Decode(&doc); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
||||||
}
|
}
|
||||||
if doc.Signatures == "" {
|
|
||||||
doc.Signatures = view.versionSignatures(docAttr.Signatures)
|
doc.Signatures = view.versionSignatures(docAttr.Signatures)
|
||||||
}
|
|
||||||
view.resourcesRead = maps.Clone(r.resourcesRead)
|
view.resourcesRead = maps.Clone(r.resourcesRead)
|
||||||
view.resourceFiles = maps.Clone(r.resourceFiles)
|
view.resourceFiles = maps.Clone(r.resourceFiles)
|
||||||
view.ResMap = maps.Clone(r.ResMap)
|
view.ResMap = maps.Clone(r.ResMap)
|
||||||
@@ -226,6 +285,14 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
view.doc, view.relatedPending = &doc, true
|
||||||
|
r.commitDocumentStructure(&view)
|
||||||
|
return r.doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitDocumentStructure 提交已完成的文档读取,保留渲染器共享的资源索引
|
||||||
|
// 入参: view 已验证的独立读取状态
|
||||||
|
func (r *Reader) commitDocumentStructure(view *Reader) {
|
||||||
maps.Copy(r.resourcesRead, view.resourcesRead)
|
maps.Copy(r.resourcesRead, view.resourcesRead)
|
||||||
maps.Copy(r.resourceFiles, view.resourceFiles)
|
maps.Copy(r.resourceFiles, view.resourceFiles)
|
||||||
maps.Copy(r.ResMap, view.ResMap)
|
maps.Copy(r.ResMap, view.ResMap)
|
||||||
@@ -235,8 +302,7 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
|
|||||||
maps.Copy(r.drawParamCache, view.drawParamCache)
|
maps.Copy(r.drawParamCache, view.drawParamCache)
|
||||||
maps.Copy(r.compositeGraphicUnitCache, view.compositeGraphicUnitCache)
|
maps.Copy(r.compositeGraphicUnitCache, view.compositeGraphicUnitCache)
|
||||||
r.documentRoot, r.versionInfo, r.RootDir = view.documentRoot, view.versionInfo, view.RootDir
|
r.documentRoot, r.versionInfo, r.RootDir = view.documentRoot, view.versionInfo, view.RootDir
|
||||||
r.doc, r.relatedPending = &doc, true
|
r.doc, r.relatedPending = view.doc, view.relatedPending
|
||||||
return r.doc, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageContent 获取页面内容
|
// PageContent 获取页面内容
|
||||||
@@ -244,7 +310,14 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
|
|||||||
// 入参: page 页面对象
|
// 入参: page 页面对象
|
||||||
// 返回: *PageContent 页面内容, error 错误信息
|
// 返回: *PageContent 页面内容, error 错误信息
|
||||||
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
||||||
return r.readPageContent(page, false, false)
|
return r.PageContentContext(context.Background(), page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageContentContext 读取独立页面内容,取消时不返回部分页面
|
||||||
|
// 入参: ctx 取消上下文, page 页面对象
|
||||||
|
// 返回: *PageContent 页面内容, error 读取、解析或取消错误
|
||||||
|
func (r *Reader) PageContentContext(ctx context.Context, page Page) (*PageContent, error) {
|
||||||
|
return r.readPageContent(ctx, page, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageCount 获取当前文档总页数
|
// PageCount 获取当前文档总页数
|
||||||
@@ -261,25 +334,39 @@ func (r *Reader) PageCount() (int, error) {
|
|||||||
// 入参: index 页面索引,从0开始
|
// 入参: index 页面索引,从0开始
|
||||||
// 返回: *PageContent 页面内容, error 错误信息
|
// 返回: *PageContent 页面内容, error 错误信息
|
||||||
func (r *Reader) PageContentByIndex(index int) (*PageContent, error) {
|
func (r *Reader) PageContentByIndex(index int) (*PageContent, error) {
|
||||||
doc, err := r.Doc()
|
return r.PageContentByIndexContext(context.Background(), index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageContentByIndexContext 按当前文档页面顺序读取独立页面内容
|
||||||
|
// 入参: ctx 取消上下文, index 页面索引,从0开始
|
||||||
|
// 返回: *PageContent 页面内容, error 索引、读取或取消错误
|
||||||
|
func (r *Reader) PageContentByIndexContext(ctx context.Context, index int) (*PageContent, error) {
|
||||||
|
doc, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if index < 0 || index >= len(doc.Pages.Page) {
|
if index < 0 || index >= len(doc.Pages.Page) {
|
||||||
return nil, fmt.Errorf("page index %d out of range", index)
|
return nil, fmt.Errorf("page index %d out of range", index)
|
||||||
}
|
}
|
||||||
return r.PageContent(doc.Pages.Page[index])
|
return r.PageContentContext(ctx, doc.Pages.Page[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageArea 读取页面区域,不解析页面图元
|
// PageArea 读取页面区域,不解析页面图元
|
||||||
// 入参: page 页面对象
|
// 入参: page 页面对象
|
||||||
// 返回: PageArea 页面区域, error 错误信息
|
// 返回: PageArea 页面区域, error 错误信息
|
||||||
func (r *Reader) PageArea(page Page) (PageArea, error) {
|
func (r *Reader) PageArea(page Page) (PageArea, error) {
|
||||||
content, err := r.readPageHeader(page)
|
return r.PageAreaContext(context.Background(), page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageAreaContext 读取页面区域并解析模板继承,不解析页面图元
|
||||||
|
// 入参: ctx 取消上下文, page 页面对象
|
||||||
|
// 返回: PageArea 页面区域, error 读取、解析或取消错误
|
||||||
|
func (r *Reader) PageAreaContext(ctx context.Context, page Page) (PageArea, error) {
|
||||||
|
content, err := r.readPageHeaderContext(ctx, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageArea{}, err
|
return PageArea{}, err
|
||||||
}
|
}
|
||||||
return r.resolvePageArea(content.Area, content.Template)
|
return r.resolvePageAreaContext(ctx, content.Area, content.Template)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResPath 按当前文档目录解析ST_Loc,包内绝对路径以根目录为基准
|
// ResPath 按当前文档目录解析ST_Loc,包内绝对路径以根目录为基准
|
||||||
@@ -842,20 +929,20 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
|
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
|
||||||
// 入参: page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
|
// 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
|
||||||
// 返回: *PageContent 页面内容, error 错误信息
|
// 返回: *PageContent 页面内容, error 错误信息
|
||||||
func (r *Reader) readPageContent(page Page, textOnly, template bool) (*PageContent, error) {
|
func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, template bool) (*PageContent, error) {
|
||||||
fullPath := r.ResPath(page.BaseLoc)
|
fullPath := r.ResPath(page.BaseLoc)
|
||||||
data, err := r.readFile(fullPath)
|
data, err := r.readFileViewContext(ctx, fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var content PageContent
|
var content PageContent
|
||||||
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
if textOnly {
|
if textOnly {
|
||||||
decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))}
|
err = xml.NewTokenDecoder(&textPageTokens{Decoder: decoder}).Decode(&content)
|
||||||
err = xml.NewTokenDecoder(decoder).Decode(&content)
|
|
||||||
} else {
|
} else {
|
||||||
err = xml.Unmarshal(data, &content)
|
err = decoder.Decode(&content)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
||||||
@@ -865,12 +952,17 @@ func (r *Reader) readPageContent(page Page, textOnly, template bool) (*PageConte
|
|||||||
content.Template = nil
|
content.Template = nil
|
||||||
}
|
}
|
||||||
for _, res := range content.PageRes {
|
for _, res := range content.PageRes {
|
||||||
r.loadRes("/" + resolveResourcePath(fullPath, "", res))
|
if err := r.loadResContext(ctx, "/"+resolveResourcePath(fullPath, "", res)); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
content.Area, err = r.resolvePageArea(content.Area, content.Template)
|
}
|
||||||
|
content.Area, err = r.resolvePageAreaContext(ctx, content.Area, content.Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &content, nil
|
return &content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,6 +982,16 @@ func (r *Reader) loadPageResources(page Page) {
|
|||||||
// 入参: page 页面对象
|
// 入参: page 页面对象
|
||||||
// 返回: PageContent 页面头部, error 错误信息
|
// 返回: PageContent 页面头部, error 错误信息
|
||||||
func (r *Reader) readPageHeader(page Page) (PageContent, error) {
|
func (r *Reader) readPageHeader(page Page) (PageContent, error) {
|
||||||
|
return r.readPageHeaderContext(context.Background(), page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPageHeaderContext 读取并缓存页面头部,取消时不缓存部分结果
|
||||||
|
// 入参: ctx 取消上下文, page 页面对象
|
||||||
|
// 返回: PageContent 页面头部, error 读取、解析或取消错误
|
||||||
|
func (r *Reader) readPageHeaderContext(ctx context.Context, page Page) (PageContent, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return PageContent{}, err
|
||||||
|
}
|
||||||
name := r.ResPath(page.BaseLoc)
|
name := r.ResPath(page.BaseLoc)
|
||||||
if content, ok := r.pageHeaderCache[name]; ok {
|
if content, ok := r.pageHeaderCache[name]; ok {
|
||||||
return content, nil
|
return content, nil
|
||||||
@@ -900,9 +1002,12 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
var content PageContent
|
var content PageContent
|
||||||
d := xml.NewDecoder(f)
|
d := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx})
|
||||||
scan:
|
scan:
|
||||||
for {
|
for {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return PageContent{}, err
|
||||||
|
}
|
||||||
token, err := d.Token()
|
token, err := d.Token()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
@@ -939,6 +1044,9 @@ scan:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return PageContent{}, err
|
||||||
|
}
|
||||||
r.pageHeaderCache[name] = content
|
r.pageHeaderCache[name] = content
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
@@ -947,15 +1055,28 @@ scan:
|
|||||||
// 入参: area 页面区域, templates 模板引用
|
// 入参: area 页面区域, templates 模板引用
|
||||||
// 返回: PageArea 页面区域, error 错误信息
|
// 返回: PageArea 页面区域, error 错误信息
|
||||||
func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) {
|
func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) {
|
||||||
|
return r.resolvePageAreaContext(context.Background(), area, templates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePageAreaContext 按模板顺序解析页面区域的继承,读取过程中响应取消
|
||||||
|
// 入参: ctx 取消上下文, area 页面区域, templates 模板引用
|
||||||
|
// 返回: PageArea 页面区域, error 读取或取消错误
|
||||||
|
func (r *Reader) resolvePageAreaContext(ctx context.Context, area PageArea, templates []Template) (PageArea, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return PageArea{}, err
|
||||||
|
}
|
||||||
if area != (PageArea{}) || r.doc == nil {
|
if area != (PageArea{}) || r.doc == nil {
|
||||||
return area, nil
|
return area, nil
|
||||||
}
|
}
|
||||||
for _, ref := range templates {
|
for _, ref := range templates {
|
||||||
for _, template := range r.doc.CommonData.TemplatePage {
|
for _, template := range r.doc.CommonData.TemplatePage {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return PageArea{}, err
|
||||||
|
}
|
||||||
if !sameResourceID(template.ID, ref.TemplateID) {
|
if !sameResourceID(template.ID, ref.TemplateID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
|
content, err := r.readPageHeaderContext(ctx, Page{BaseLoc: template.BaseLoc})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PageArea{}, err
|
return PageArea{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,13 +235,13 @@ func (r *Reader) documentDependencies(ctx context.Context, indexes ...int) (map[
|
|||||||
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝无法识别的扩展节点, add 接收引用路径及类别
|
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝无法识别的扩展节点, add 接收引用路径及类别
|
||||||
// 返回: error 读取或结构错误
|
// 返回: error 读取或结构错误
|
||||||
func (r *Reader) documentFileReferences(ctx context.Context, name string, rejectUnknown bool, add func(string, int)) error {
|
func (r *Reader) documentFileReferences(ctx context.Context, name string, rejectUnknown bool, add func(string, int)) error {
|
||||||
return r.scanDocumentFileReferences(ctx, name, rejectUnknown, add, nil)
|
return r.scanDocumentFileReferences(ctx, name, rejectUnknown, add, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanDocumentFileReferences 流式扫描文件依赖,按需记录引用节点位置,不构建完整节点树
|
// scanDocumentFileReferences 流式扫描文件依赖,按需记录引用节点位置,不构建完整节点树
|
||||||
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝未知扩展, add 接收依赖, record 接收引用位置,可为nil
|
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝未知扩展, add 接收依赖, record 接收引用位置,可为nil, inspect 检查节点,可为nil
|
||||||
// 返回: error 读取、结构或取消错误
|
// 返回: error 读取、结构或取消错误
|
||||||
func (r *Reader) scanDocumentFileReferences(ctx context.Context, name string, rejectUnknown bool, add func(string, int), record func(*editorXML, string, string)) error {
|
func (r *Reader) scanDocumentFileReferences(ctx context.Context, name string, rejectUnknown bool, add func(string, int), record func(*editorXML, string, string), inspect func(xml.StartElement) error) error {
|
||||||
input, err := r.openFile(name)
|
input, err := r.openFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -269,6 +269,11 @@ func (r *Reader) scanDocumentFileReferences(ctx context.Context, name string, re
|
|||||||
}
|
}
|
||||||
switch node := token.(type) {
|
switch node := token.(type) {
|
||||||
case xml.StartElement:
|
case xml.StartElement:
|
||||||
|
if inspect != nil {
|
||||||
|
if err := inspect(node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
parent := ""
|
parent := ""
|
||||||
if len(stack) == 0 {
|
if len(stack) == 0 {
|
||||||
if root != "" {
|
if root != "" {
|
||||||
|
|||||||
+7
-57
@@ -18,7 +18,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// semanticCompiler 将公共页面语义编译为路径和原始图片,绘制、搜索和度量共享定位
|
// semanticCompiler 将公共页面语义编译为路径和原始图片,绘制、搜索和度量共享定位
|
||||||
@@ -296,8 +295,9 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
|||||||
if defaults.Join != "" {
|
if defaults.Join != "" {
|
||||||
style.options.Join = defaults.Join
|
style.options.Join = defaults.Join
|
||||||
}
|
}
|
||||||
if defaults.MiterLimit > 0 {
|
if defaults.MiterLimit > 0 || defaults.MiterLimitSet {
|
||||||
style.options.MiterLimit = defaults.MiterLimit
|
style.options.MiterLimit = defaults.MiterLimit
|
||||||
|
style.options.MiterLimitSet = defaults.MiterLimitSet
|
||||||
}
|
}
|
||||||
style.options.Dashes = parseFloats(defaults.DashPattern)
|
style.options.Dashes = parseFloats(defaults.DashPattern)
|
||||||
if defaults.DashOffset != nil {
|
if defaults.DashOffset != nil {
|
||||||
@@ -319,8 +319,9 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
|||||||
if object.Join != "" {
|
if object.Join != "" {
|
||||||
style.options.Join = object.Join
|
style.options.Join = object.Join
|
||||||
}
|
}
|
||||||
if object.MiterLimit > 0 {
|
if object.MiterLimit > 0 || object.MiterLimitSet {
|
||||||
style.options.MiterLimit = object.MiterLimit
|
style.options.MiterLimit = object.MiterLimit
|
||||||
|
style.options.MiterLimitSet = object.MiterLimitSet
|
||||||
}
|
}
|
||||||
if object.DashPattern != "" || object.dashPatternSet {
|
if object.DashPattern != "" || object.dashPatternSet {
|
||||||
style.options.Dashes = parseFloats(object.DashPattern)
|
style.options.Dashes = parseFloats(object.DashPattern)
|
||||||
@@ -337,9 +338,7 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
|
|||||||
style.options.Dashes[i] *= scale
|
style.options.Dashes[i] *= scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if style.options.Width == 0 {
|
style.options.Width = c.renderer.deviceLineWidth(style.options.Width)
|
||||||
style.options.Width = 25.4 / c.renderer.DPI
|
|
||||||
}
|
|
||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,24 +353,11 @@ func (c *semanticCompiler) pathObject(object PathObject, state RenderState) erro
|
|||||||
local := object
|
local := object
|
||||||
local.Boundary = "0 0 1 1"
|
local.Boundary = "0 0 1 1"
|
||||||
local.CTM = ""
|
local.CTM = ""
|
||||||
var path GeometryPath
|
path, err := c.geometry.Path(local)
|
||||||
var err error
|
|
||||||
tinyMatrix := matrix
|
|
||||||
if state.BoundaryInCTM && state.Parent != nil {
|
|
||||||
tinyMatrix, _ = renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), RenderState{})
|
|
||||||
}
|
|
||||||
path = tinyFillRectPath(object, tinyMatrix)
|
|
||||||
if path != nil {
|
|
||||||
if state.BoundaryInCTM && state.Parent != nil {
|
|
||||||
path, err = c.geometry.Transform(path, *state.Parent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
path, err = c.geometry.Path(local)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
path, err = c.geometry.Transform(path, matrix)
|
path, err = c.geometry.Transform(path, matrix)
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -427,7 +413,7 @@ func (c *semanticCompiler) textObject(object TextObject, state RenderState) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, linear := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
|
_, linear := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
|
||||||
style := c.objectStyle(PathObject{DrawParam: object.DrawParam, FillColor: object.FillColor, StrokeColor: object.StrokeColor, Alpha: object.Alpha, LineWidth: object.LineWidth, LineWidthSet: object.LineWidthSet, Join: object.Join, MiterLimit: object.MiterLimit}, state.Defaults, linear)
|
style := c.objectStyle(object.pathStyle(), state.Defaults, linear)
|
||||||
if style.fill == nil {
|
if style.fill == nil {
|
||||||
style.fill = withFillAlpha(&FillColor{}, object.Alpha)
|
style.fill = withFillAlpha(&FillColor{}, object.Alpha)
|
||||||
}
|
}
|
||||||
@@ -511,39 +497,3 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tinyFillRectPath 保留历史文档中微小填充块的缺省末端处理
|
|
||||||
// 入参: object 路径对象, matrix 不含外层变换的页面矩阵
|
|
||||||
// 返回: GeometryPath 修正路径,非匹配格式时为空
|
|
||||||
func tinyFillRectPath(object PathObject, matrix Matrix) GeometryPath {
|
|
||||||
if object.Fill == nil || !*object.Fill || object.Stroke == nil || *object.Stroke {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
box, err := ParseBox(object.Boundary)
|
|
||||||
if err != nil || box.W <= 0 || box.H <= 0 || box.W > .6 || box.H > .6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tokens := strings.Fields(object.AbbreviatedData)
|
|
||||||
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var minX, minY, maxX, maxY float64
|
|
||||||
for i := 1; i < 10; i += 3 {
|
|
||||||
x, errX := strconv.ParseFloat(tokens[i], 64)
|
|
||||||
y, errY := strconv.ParseFloat(tokens[i+1], 64)
|
|
||||||
if errX != nil || errY != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
x, y = matrix.Transform(x, y)
|
|
||||||
if i == 1 {
|
|
||||||
minX, maxX, minY, maxY = x, x, y, y
|
|
||||||
} else {
|
|
||||||
minX = math.Min(minX, x)
|
|
||||||
maxX = math.Max(maxX, x)
|
|
||||||
minY = math.Min(minY, y)
|
|
||||||
maxY = math.Max(maxY, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expand := math.Min(box.W, box.H) * .08
|
|
||||||
return geometryRectangle(Box{X: minX - expand, Y: minY - expand, W: maxX - minX + 2*expand, H: maxY - minY + 2*expand})
|
|
||||||
}
|
|
||||||
@@ -140,8 +140,9 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
|
|||||||
angle := float64(object.CharDirection) * math.Pi / 180
|
angle := float64(object.CharDirection) * math.Pi / 180
|
||||||
rotate := Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}
|
rotate := Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}
|
||||||
glyphLinear := Matrix{a: linear.a, b: linear.b, c: linear.c, d: linear.d}.Multiply(rotate)
|
glyphLinear := Matrix{a: linear.a, b: linear.b, c: linear.c, d: linear.d}.Multiply(rotate)
|
||||||
for index, code := range object.TextCode {
|
lines := newTextLineScanner(object)
|
||||||
if object.textCodeLineBreak(index) {
|
for _, code := range object.TextCode {
|
||||||
|
if lines.next(code) {
|
||||||
textOffset++
|
textOffset++
|
||||||
}
|
}
|
||||||
runes := textCodeRunes(code.Value)
|
runes := textCodeRunes(code.Value)
|
||||||
|
|||||||
+22
-1
@@ -61,6 +61,20 @@ type Renderer struct {
|
|||||||
TransparentBackground bool
|
TransparentBackground bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deviceLineWidth 按设备分辨率计算描边宽度,零线宽为一像素,正线宽至少为两像素
|
||||||
|
// 入参: width 页面空间线宽,单位为毫米
|
||||||
|
// 返回: float64 设备描边宽度,单位为毫米
|
||||||
|
func (r *Renderer) deviceLineWidth(width float64) float64 {
|
||||||
|
pixel := 25.4 / r.DPI
|
||||||
|
if width == 0 {
|
||||||
|
return pixel
|
||||||
|
}
|
||||||
|
if width > 0 {
|
||||||
|
return max(width, 2*pixel)
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
// RendererOption 渲染器配置选项
|
// RendererOption 渲染器配置选项
|
||||||
type RendererOption func(*Renderer)
|
type RendererOption func(*Renderer)
|
||||||
|
|
||||||
@@ -117,6 +131,13 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
|||||||
// 入参: templateID 模板ID
|
// 入参: templateID 模板ID
|
||||||
// 返回: *PageContent 模板页面
|
// 返回: *PageContent 模板页面
|
||||||
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
||||||
|
ctx := r.pageContext
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = r.outputContext()
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if key := editorResourceID(templateID); key != "" {
|
if key := editorResourceID(templateID); key != "" {
|
||||||
templateID = key
|
templateID = key
|
||||||
}
|
}
|
||||||
@@ -127,7 +148,7 @@ func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
|||||||
if !sameResourceID(tpl.ID, templateID) {
|
if !sameResourceID(tpl.ID, templateID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
page, err := r.Reader.readPageContent(Page{BaseLoc: tpl.BaseLoc}, false, true)
|
page, err := r.Reader.readPageContent(ctx, Page{BaseLoc: tpl.BaseLoc}, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ type rasterStrokeGradientCanvas struct {
|
|||||||
type rasterGradientCanvas struct {
|
type rasterGradientCanvas struct {
|
||||||
gradient *RasterGradient
|
gradient *RasterGradient
|
||||||
space canvas.ColorSpace
|
space canvas.ColorSpace
|
||||||
|
mesh *MeshSampler
|
||||||
|
}
|
||||||
|
|
||||||
|
// newRasterGradientCanvas 准备渐变适配器及独立网格采样器
|
||||||
|
// 入参: gradient 公共渐变
|
||||||
|
// 返回: rasterGradientCanvas Canvas渐变
|
||||||
|
func newRasterGradientCanvas(gradient *RasterGradient) rasterGradientCanvas {
|
||||||
|
result := rasterGradientCanvas{gradient: gradient}
|
||||||
|
if gradient.Kind == RasterMesh {
|
||||||
|
result.mesh = gradient.Mesh.NewSampler()
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name 返回后端标识
|
// Name 返回后端标识
|
||||||
@@ -123,7 +135,9 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
paint := canvas.Paint{Color: cmd.Paint.Color}
|
paint := canvas.Paint{Color: cmd.Paint.Color}
|
||||||
|
var sampled rasterGradientCanvas
|
||||||
if g := cmd.Paint.Gradient; g != nil {
|
if g := cmd.Paint.Gradient; g != nil {
|
||||||
|
sampled = newRasterGradientCanvas(g)
|
||||||
stops := make(canvas.Grad, len(g.Stops))
|
stops := make(canvas.Grad, len(g.Stops))
|
||||||
for j, stop := range g.Stops {
|
for j, stop := range g.Stops {
|
||||||
stops[j] = canvas.Stop{Offset: stop.Offset, Color: stop.Color}
|
stops[j] = canvas.Stop{Offset: stop.Offset, Color: stop.Color}
|
||||||
@@ -135,12 +149,12 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
|||||||
case RasterRadial:
|
case RasterRadial:
|
||||||
paint = canvas.Paint{Gradient: stops.ToRadial(start, g.R0, end, g.R1)}
|
paint = canvas.Paint{Gradient: stops.ToRadial(start, g.R0, end, g.R1)}
|
||||||
case RasterMesh:
|
case RasterMesh:
|
||||||
paint = canvas.Paint{Gradient: rasterGradientCanvas{gradient: g}}
|
paint = canvas.Paint{Gradient: sampled}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind)
|
return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind)
|
||||||
}
|
}
|
||||||
if g.Spread != nil || g.Sample != nil {
|
if g.Spread != nil || g.Sample != nil {
|
||||||
paint = canvas.Paint{Gradient: rasterGradientCanvas{gradient: g}}
|
paint = canvas.Paint{Gradient: sampled}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style := canvas.Style{Fill: paint, FillRule: canvas.NonZero}
|
style := canvas.Style{Fill: paint, FillRule: canvas.NonZero}
|
||||||
@@ -159,10 +173,10 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
paint = canvas.Paint{Gradient: rasterStrokeGradientCanvas{rasterGradientCanvas{gradient: cmd.Paint.Gradient}, inverse}}
|
paint = canvas.Paint{Gradient: rasterStrokeGradientCanvas{sampled, inverse}}
|
||||||
}
|
}
|
||||||
line := pathStyle{lineJoin: canvas.MiterJoin, miterLimit: defaultMiterLimit}
|
line := pathStyle{lineJoin: canvas.MiterJoin, miterLimit: defaultMiterLimit}
|
||||||
line.applyLineJoin(options.Join, options.MiterLimit)
|
line.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet)
|
||||||
style = canvas.Style{Stroke: paint, StrokeWidth: options.Width, StrokeCapper: pathLineCap(options.Cap, canvas.ButtCap), StrokeJoiner: line.lineJoin, FillRule: canvas.NonZero}
|
style = canvas.Style{Stroke: paint, StrokeWidth: options.Width, StrokeCapper: pathLineCap(options.Cap, canvas.ButtCap), StrokeJoiner: line.lineJoin, FillRule: canvas.NonZero}
|
||||||
if len(options.Dashes) > 0 {
|
if len(options.Dashes) > 0 {
|
||||||
style.DashOffset = options.DashOffset / options.Width
|
style.DashOffset = options.DashOffset / options.Width
|
||||||
@@ -224,7 +238,12 @@ func (g rasterStrokeGradientCanvas) SetColorSpace(space canvas.ColorSpace) canva
|
|||||||
// 入参: x 横坐标, y 纵坐标
|
// 入参: x 横坐标, y 纵坐标
|
||||||
// 返回: color.RGBA 渐变颜色
|
// 返回: color.RGBA 渐变颜色
|
||||||
func (g rasterGradientCanvas) At(x, y float64) color.RGBA {
|
func (g rasterGradientCanvas) At(x, y float64) color.RGBA {
|
||||||
value := g.gradient.At(x, y)
|
var value color.RGBA
|
||||||
|
if g.mesh != nil {
|
||||||
|
value = g.mesh.At(x, y)
|
||||||
|
} else {
|
||||||
|
value = g.gradient.At(x, y)
|
||||||
|
}
|
||||||
if g.space != nil {
|
if g.space != nil {
|
||||||
return g.space.ToLinear(value)
|
return g.space.ToLinear(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -119,6 +120,9 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ma
|
|||||||
textObj.FillColor = &FillColor{Value: "0 0 0"}
|
textObj.FillColor = &FillColor{Value: "0 0 0"}
|
||||||
textObj.StrokeColor = &StrokeColor{Value: "0 0 0"}
|
textObj.StrokeColor = &StrokeColor{Value: "0 0 0"}
|
||||||
textRenderer := *r
|
textRenderer := *r
|
||||||
|
if scale := math.Sqrt(math.Abs(m.a*m.d - m.b*m.c)); scale > 0 {
|
||||||
|
textRenderer.DPI *= scale
|
||||||
|
}
|
||||||
textRenderer.pageText = nil
|
textRenderer.pageText = nil
|
||||||
textRenderer.textOnly = false
|
textRenderer.textOnly = false
|
||||||
textRenderer.renderText(ctx, textObj, 0, r.drawParamDefaults(area.DrawParam, nil), nil, false, nil)
|
textRenderer.renderText(ctx, textObj, 0, r.drawParamDefaults(area.DrawParam, nil), nil, false, nil)
|
||||||
@@ -170,8 +174,10 @@ func (r *Renderer) buildClipAreaPath(obj PathObject, drawParam string, parent Ma
|
|||||||
obj.FillColor, obj.StrokeColor = nil, nil
|
obj.FillColor, obj.StrokeColor = nil, nil
|
||||||
style := r.newPathStyle(dp, 0, 0, 0, nil)
|
style := r.newPathStyle(dp, 0, 0, 0, nil)
|
||||||
style.applyPathObject(r, obj, 0, 0, 0)
|
style.applyPathObject(r, obj, 0, 0, 0)
|
||||||
if style.lineWidth == 0 {
|
if scale := math.Sqrt(math.Abs(m.a*m.d - m.b*m.c)); scale > 0 {
|
||||||
style.lineWidth = 25.4 / r.DPI
|
style.lineWidth = r.deviceLineWidth(style.lineWidth*scale) / scale
|
||||||
|
} else {
|
||||||
|
style.lineWidth = r.deviceLineWidth(style.lineWidth)
|
||||||
}
|
}
|
||||||
result = unionClipPath(result, r.strokeDashedCanvasPath(p, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern, false))
|
result = unionClipPath(result, r.strokeDashedCanvasPath(p, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern, false))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
|
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
|
||||||
style.applyLineJoin(options.Join, options.MiterLimit)
|
style.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet)
|
||||||
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
||||||
p = p.Dash(options.DashOffset, options.Dashes...)
|
p = p.Dash(options.DashOffset, options.Dashes...)
|
||||||
p = p.Flatten(tolerance)
|
p = p.Flatten(tolerance)
|
||||||
@@ -224,6 +224,7 @@ func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, t
|
|||||||
options.Join = "Bevel"
|
options.Join = "Bevel"
|
||||||
case canvas.MiterJoiner:
|
case canvas.MiterJoiner:
|
||||||
options.MiterLimit = value.Limit
|
options.MiterLimit = value.Limit
|
||||||
|
options.MiterLimitSet = value.Limit == 0
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any
|
|||||||
case PaintLinear, PaintRadial:
|
case PaintLinear, PaintRadial:
|
||||||
return canvasShading(paint, x, y, pageH)
|
return canvasShading(paint, x, y, pageH)
|
||||||
case PaintMesh:
|
case PaintMesh:
|
||||||
return &shdPaint{gradient: rasterGradientCanvas{gradient: &RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh}}, view: canvas.Matrix{{1, 0, x}, {0, -1, pageH - y}}}
|
return &shdPaint{gradient: newRasterGradientCanvas(&RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh}), view: canvas.Matrix{{1, 0, x}, {0, -1, pageH - y}}}
|
||||||
default:
|
default:
|
||||||
return paint.Color
|
return paint.Color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type pathStyle struct {
|
|||||||
lineWidth float64
|
lineWidth float64
|
||||||
lineCap canvas.Capper
|
lineCap canvas.Capper
|
||||||
lineJoin canvas.Joiner
|
lineJoin canvas.Joiner
|
||||||
|
lineJoinName string
|
||||||
miterLimit float64
|
miterLimit float64
|
||||||
dashOffset float64
|
dashOffset float64
|
||||||
dashPattern []float64
|
dashPattern []float64
|
||||||
@@ -90,21 +91,35 @@ func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// applyLineJoin 应用线连接样式及尖角限制
|
// applyLineJoin 应用线连接样式及尖角限制
|
||||||
// 入参: join 线连接名称, limit 尖角限制
|
// 入参: join 线连接名称, limit 尖角限制, explicit 是否显式指定零截断值
|
||||||
func (s *pathStyle) applyLineJoin(join string, limit float64) {
|
func (s *pathStyle) applyLineJoin(join string, limit float64, explicit bool) {
|
||||||
if limit > 0 {
|
if limit > 0 || explicit {
|
||||||
s.miterLimit = limit
|
s.miterLimit = limit
|
||||||
}
|
}
|
||||||
|
if s.lineJoinName == "" {
|
||||||
|
switch s.lineJoin.(type) {
|
||||||
|
case canvas.RoundJoiner:
|
||||||
|
s.lineJoinName = "Round"
|
||||||
|
case canvas.BevelJoiner:
|
||||||
|
s.lineJoinName = "Bevel"
|
||||||
|
default:
|
||||||
|
s.lineJoinName = "Miter"
|
||||||
|
}
|
||||||
|
}
|
||||||
switch join {
|
switch join {
|
||||||
case "Miter":
|
case "Miter", "Round", "Bevel":
|
||||||
s.lineJoin = canvas.MiterJoin
|
s.lineJoinName = join
|
||||||
|
}
|
||||||
|
switch s.lineJoinName {
|
||||||
case "Round":
|
case "Round":
|
||||||
s.lineJoin = canvas.RoundJoin
|
s.lineJoin = canvas.RoundJoin
|
||||||
case "Bevel":
|
case "Bevel":
|
||||||
s.lineJoin = canvas.BevelJoin
|
s.lineJoin = canvas.BevelJoin
|
||||||
}
|
default:
|
||||||
if _, ok := s.lineJoin.(canvas.MiterJoiner); ok {
|
|
||||||
s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit}
|
s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit}
|
||||||
|
if s.miterLimit <= 1 {
|
||||||
|
s.lineJoin = canvas.BevelJoin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +138,7 @@ func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH flo
|
|||||||
if dp.Cap != "" {
|
if dp.Cap != "" {
|
||||||
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
||||||
}
|
}
|
||||||
s.applyLineJoin(dp.Join, dp.MiterLimit)
|
s.applyLineJoin(dp.Join, dp.MiterLimit, dp.MiterLimitSet)
|
||||||
if dp.DashPattern != "" || dp.dashPatternSet {
|
if dp.DashPattern != "" || dp.dashPatternSet {
|
||||||
s.dashPattern = parseFloats(dp.DashPattern)
|
s.dashPattern = parseFloats(dp.DashPattern)
|
||||||
}
|
}
|
||||||
@@ -147,7 +162,7 @@ func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH f
|
|||||||
if obj.Cap != "" {
|
if obj.Cap != "" {
|
||||||
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
||||||
}
|
}
|
||||||
s.applyLineJoin(obj.Join, obj.MiterLimit)
|
s.applyLineJoin(obj.Join, obj.MiterLimit, obj.MiterLimitSet)
|
||||||
if obj.DashPattern != "" || obj.dashPatternSet {
|
if obj.DashPattern != "" || obj.dashPatternSet {
|
||||||
s.dashPattern = parseFloats(obj.DashPattern)
|
s.dashPattern = parseFloats(obj.DashPattern)
|
||||||
}
|
}
|
||||||
@@ -213,9 +228,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
}
|
}
|
||||||
style.applyPathObject(r, obj, bx, by, pageH)
|
style.applyPathObject(r, obj, bx, by, pageH)
|
||||||
style.scale(ctm)
|
style.scale(ctm)
|
||||||
if style.lineWidth == 0 {
|
style.lineWidth = r.deviceLineWidth(style.lineWidth)
|
||||||
style.lineWidth = 25.4 / r.DPI
|
|
||||||
}
|
|
||||||
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
|
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
|
||||||
if boundaryInCTM && parentCTM != nil {
|
if boundaryInCTM && parentCTM != nil {
|
||||||
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
|
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
|
||||||
@@ -226,10 +239,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
if boundaryInCTM {
|
if boundaryInCTM {
|
||||||
pathCTM = localCTM
|
pathCTM = localCTM
|
||||||
}
|
}
|
||||||
p := r.buildTinyFillRectPath(obj, pageH, pathCTM, bx, by)
|
p := r.buildPath(obj, pageH, pathCTM, false)
|
||||||
if p == nil {
|
|
||||||
p = r.buildPath(obj, pageH, pathCTM, false)
|
|
||||||
}
|
|
||||||
if boundaryInCTM && parentCTM != nil {
|
if boundaryInCTM && parentCTM != nil {
|
||||||
p = p.Transform(canvas.Matrix{
|
p = p.Transform(canvas.Matrix{
|
||||||
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e},
|
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e},
|
||||||
@@ -471,19 +481,3 @@ func (r *Renderer) parseCanvasPath(data string) (*canvas.Path, error) {
|
|||||||
}
|
}
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTinyFillRectPath 构建微小填充矩形路径
|
|
||||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
|
|
||||||
// 返回: *canvas.Path 路径对象
|
|
||||||
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
|
|
||||||
path := tinyFillRectPath(obj, TranslationMatrix(bx, by).Multiply(ctm))
|
|
||||||
if path == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
p, err := geometryToCanvasPath(&path)
|
|
||||||
if err != nil {
|
|
||||||
r.renderError = err
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p.Translate(0, pageH)
|
|
||||||
}
|
|
||||||
@@ -108,20 +108,14 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if dp != nil {
|
if dp != nil {
|
||||||
strokeStyle.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha)
|
strokeStyle.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha)
|
||||||
}
|
}
|
||||||
if obj.StrokeColor != nil {
|
strokeObject := obj.pathStyle()
|
||||||
strokeStyle.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
strokeObject.FillColor = nil
|
||||||
}
|
strokeStyle.applyPathObject(r, strokeObject, bx, by, pageH)
|
||||||
if obj.LineWidth > 0 || obj.LineWidthSet {
|
|
||||||
strokeStyle.lineWidth = obj.LineWidth
|
|
||||||
}
|
|
||||||
strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit)
|
|
||||||
if strokeStyle.strokePaint == nil {
|
if strokeStyle.strokePaint == nil {
|
||||||
strokeStyle.strokePaint = canvas.Transparent
|
strokeStyle.strokePaint = canvas.Transparent
|
||||||
}
|
}
|
||||||
strokeStyle.scale(ctm)
|
strokeStyle.scale(ctm)
|
||||||
if strokeStyle.lineWidth == 0 {
|
strokeStyle.lineWidth = r.deviceLineWidth(strokeStyle.lineWidth)
|
||||||
strokeStyle.lineWidth = 25.4 / r.DPI
|
|
||||||
}
|
|
||||||
transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
|
transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
|
||||||
strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint)
|
strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint)
|
||||||
strokeClip = r.intersectCachedClipPath(strokeClip, shadingClip)
|
strokeClip = r.intersectCachedClipPath(strokeClip, shadingClip)
|
||||||
@@ -220,8 +214,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
codePos := 0
|
codePos := 0
|
||||||
textPos := 0
|
textPos := 0
|
||||||
for index, tc := range obj.TextCode {
|
var lines textLineScanner
|
||||||
if textRun != nil && obj.textCodeLineBreak(index) {
|
if textRun != nil {
|
||||||
|
lines = newTextLineScanner(obj)
|
||||||
|
}
|
||||||
|
for _, tc := range obj.TextCode {
|
||||||
|
if textRun != nil && lines.next(tc) {
|
||||||
textPos++
|
textPos++
|
||||||
}
|
}
|
||||||
var runes []rune
|
var runes []rune
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ type DrawParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DrawParam 绘制参数
|
// DrawParam 绘制参数
|
||||||
|
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
|
||||||
type DrawParam struct {
|
type DrawParam struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
Relative string `xml:"Relative,attr"`
|
Relative string `xml:"Relative,attr"`
|
||||||
@@ -98,6 +99,7 @@ type DrawParam struct {
|
|||||||
FillColor *FillColor `xml:"FillColor"`
|
FillColor *FillColor `xml:"FillColor"`
|
||||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||||
LineWidthSet bool `xml:"-"`
|
LineWidthSet bool `xml:"-"`
|
||||||
|
MiterLimitSet bool `xml:"-"`
|
||||||
dashPatternSet bool
|
dashPatternSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -106,14 +106,18 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
|
|||||||
// 入参: index 页面索引,从0开始
|
// 入参: index 页面索引,从0开始
|
||||||
// 返回: *PageText 页面文字, error 错误信息
|
// 返回: *PageText 页面文字, error 错误信息
|
||||||
func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
|
func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
|
||||||
doc, err := r.Reader.Doc()
|
ctx := r.pageContext
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = r.outputContext()
|
||||||
|
}
|
||||||
|
doc, err := r.Reader.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if index < 0 || index >= len(doc.Pages.Page) {
|
if index < 0 || index >= len(doc.Pages.Page) {
|
||||||
return nil, fmt.Errorf("page index %d out of range", index)
|
return nil, fmt.Errorf("page index %d out of range", index)
|
||||||
}
|
}
|
||||||
page, err := r.Reader.readPageContent(doc.Pages.Page[index], true, false)
|
page, err := r.Reader.readPageContent(ctx, doc.Pages.Page[index], true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -205,7 +205,7 @@ func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
|
|||||||
if box == previous {
|
if box == previous {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if err := validateCreationBox(box); err != nil {
|
||||||
return PathObject{}, err
|
return PathObject{}, err
|
||||||
}
|
}
|
||||||
shape, err := NewShape(kind, Box{W: box.W, H: box.H})
|
shape, err := NewShape(kind, Box{W: box.W, H: box.H})
|
||||||
@@ -214,7 +214,7 @@ func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
|
|||||||
}
|
}
|
||||||
matrix = matrix.Multiply(TranslationMatrix(box.X, box.Y))
|
matrix = matrix.Multiply(TranslationMatrix(box.X, box.Y))
|
||||||
bounds := matrix.TransformBox(Box{W: box.W, H: box.H})
|
bounds := matrix.TransformBox(Box{W: box.W, H: box.H})
|
||||||
if _, err := creationBox(editorBoxString(bounds)); err != nil {
|
if err := validateCreationBox(bounds); err != nil {
|
||||||
return PathObject{}, err
|
return PathObject{}, err
|
||||||
}
|
}
|
||||||
p.Boundary = editorBoxString(bounds)
|
p.Boundary = editorBoxString(bounds)
|
||||||
|
|||||||
+18
-7
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@@ -167,9 +168,12 @@ func parseSignatureStampBox(s string) (Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseSignatures 解析签名文件
|
// parseSignatures 解析签名文件
|
||||||
// 入参: doc 文档结构
|
// 入参: ctx 取消上下文, doc 文档结构
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Reader) parseSignatures(doc *Document) error {
|
func (r *Reader) parseSignatures(ctx context.Context, doc *Document) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if doc.Signatures == "" {
|
if doc.Signatures == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -180,11 +184,14 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
var signatures Signatures
|
var signatures Signatures
|
||||||
if err := xml.NewDecoder(f).Decode(&signatures); err != nil {
|
if err := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx}).Decode(&signatures); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pages := signaturePageNumbers(doc)
|
pages := signaturePageNumbers(doc)
|
||||||
for _, sigRef := range signatures.List {
|
for _, sigRef := range signatures.List {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
func(sigRef Signature) {
|
func(sigRef Signature) {
|
||||||
sigPath := resolveResourcePath(sigListPath, "", sigRef.BaseLoc)
|
sigPath := resolveResourcePath(sigListPath, "", sigRef.BaseLoc)
|
||||||
sf, err := r.openFile(sigPath)
|
sf, err := r.openFile(sigPath)
|
||||||
@@ -193,20 +200,20 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
}
|
}
|
||||||
defer sf.Close()
|
defer sf.Close()
|
||||||
var sigFile SignatureFile
|
var sigFile SignatureFile
|
||||||
if err := xml.NewDecoder(sf).Decode(&sigFile); err != nil {
|
if err := xml.NewDecoder(imageInput{ReadCloser: sf, context: ctx}).Decode(&sigFile); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var sealType string
|
var sealType string
|
||||||
var sealData []byte
|
var sealData []byte
|
||||||
if sigFile.SignedInfo.Seal.BaseLoc != "" {
|
if sigFile.SignedInfo.Seal.BaseLoc != "" {
|
||||||
sealPath := resolveResourcePath(sigPath, "", sigFile.SignedInfo.Seal.BaseLoc)
|
sealPath := resolveResourcePath(sigPath, "", sigFile.SignedInfo.Seal.BaseLoc)
|
||||||
if data, err := r.readFile(sealPath); err == nil {
|
if data, err := r.readFileViewContext(ctx, sealPath); err == nil {
|
||||||
sealType, sealData = extractSeal(data)
|
sealType, sealData = extractSeal(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(sealData) == 0 && sigFile.SignedValue != "" {
|
if len(sealData) == 0 && sigFile.SignedValue != "" {
|
||||||
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
|
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
|
||||||
if data, err := r.readFile(signedValuePath); err == nil {
|
if data, err := r.readFileViewContext(ctx, signedValuePath); err == nil {
|
||||||
if sig, err := parseSESSignature(data); err == nil {
|
if sig, err := parseSESSignature(data); err == nil {
|
||||||
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
|
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +224,11 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
if len(sealData) == 0 {
|
if len(sealData) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
sealData = bytes.Clone(sealData)
|
||||||
for _, annot := range sigFile.SignedInfo.StampAnnot {
|
for _, annot := range sigFile.SignedInfo.StampAnnot {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
page, ok := resourceValue(pages, annot.PageRef)
|
page, ok := resourceValue(pages, annot.PageRef)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -239,7 +250,7 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
}
|
}
|
||||||
}(sigRef)
|
}(sigRef)
|
||||||
}
|
}
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// signaturePageNumbers 获取签名页面编号索引
|
// signaturePageNumbers 获取签名页面编号索引
|
||||||
|
|||||||
@@ -322,9 +322,7 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
|
|||||||
if err := xml.Unmarshal(data, &doc); err != nil {
|
if err := xml.Unmarshal(data, &doc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if doc.Signatures == "" {
|
|
||||||
doc.Signatures = view.versionSignatures(body.Signatures)
|
doc.Signatures = view.versionSignatures(body.Signatures)
|
||||||
}
|
|
||||||
if doc.Signatures == "" {
|
if doc.Signatures == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-117
@@ -34,13 +34,35 @@ type textGlyphTransform struct {
|
|||||||
Glyphs []textGlyph
|
Glyphs []textGlyph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pathStyle 提取文字绘制属性和变换,供路径描边处理复用
|
||||||
|
// 返回: PathObject 绘制属性
|
||||||
|
func (obj TextObject) pathStyle() PathObject {
|
||||||
|
return PathObject{CTM: obj.CTM, DrawParam: obj.DrawParam, Alpha: obj.Alpha, Fill: obj.Fill, Stroke: obj.Stroke,
|
||||||
|
FillColor: obj.FillColor, StrokeColor: obj.StrokeColor, LineWidth: obj.LineWidth, LineWidthSet: obj.LineWidthSet,
|
||||||
|
Cap: obj.Cap, Join: obj.Join, MiterLimit: obj.MiterLimit, MiterLimitSet: obj.MiterLimitSet, DashPattern: obj.DashPattern,
|
||||||
|
dashPatternSet: obj.dashPatternSet, DashOffset: obj.DashOffset}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withPathStyle 替换共用绘制属性,保留文字内容、字体及定位
|
||||||
|
// 入参: style 绘制属性
|
||||||
|
// 返回: TextObject 修改后的文字对象
|
||||||
|
func (obj TextObject) withPathStyle(style PathObject) TextObject {
|
||||||
|
obj.DrawParam, obj.Alpha, obj.Fill, obj.Stroke = style.DrawParam, style.Alpha, style.Fill, style.Stroke
|
||||||
|
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
|
||||||
|
obj.LineWidth, obj.LineWidthSet = style.LineWidth, style.LineWidthSet
|
||||||
|
obj.Cap, obj.Join, obj.MiterLimit = style.Cap, style.Join, style.MiterLimit
|
||||||
|
obj.MiterLimitSet = style.MiterLimitSet
|
||||||
|
obj.DashPattern, obj.dashPatternSet, obj.DashOffset = style.DashPattern, style.dashPatternSet, style.DashOffset
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
|
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
|
||||||
// 返回: string 原文
|
// 返回: string 原文
|
||||||
func (obj TextObject) Text() string {
|
func (obj TextObject) Text() string {
|
||||||
obj.TextCode = textCodeOrigins(obj.TextCode, 0)
|
lines := newTextLineScanner(obj)
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
for i, code := range obj.TextCode {
|
for _, code := range obj.TextCode {
|
||||||
if obj.textCodeLineBreak(i) {
|
if lines.next(code) {
|
||||||
text.WriteByte('\n')
|
text.WriteByte('\n')
|
||||||
}
|
}
|
||||||
if code.Index == "" {
|
if code.Index == "" {
|
||||||
@@ -64,120 +86,6 @@ func (tc *TextCode) GetDeltaY() []float64 {
|
|||||||
return parseFloats(tc.DeltaY)
|
return parseFloats(tc.DeltaY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// textCodeLineBreak 根据行首、行距和字形位置推断横向文本换行
|
|
||||||
// 入参: index 文本编码索引
|
|
||||||
// 返回: bool 是否换行
|
|
||||||
func (obj TextObject) textCodeLineBreak(index int) bool {
|
|
||||||
if index == 0 || obj.ReadDirection != 0 || obj.CharDirection != 0 || obj.TextCode[index].Y == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
code, previous := obj.TextCode[index], obj.TextCode[index-1]
|
|
||||||
for i := index - 2; i >= 0 && (previous.X == "" || previous.Y == ""); i-- {
|
|
||||||
if previous.X == "" {
|
|
||||||
previous.X = obj.TextCode[i].X
|
|
||||||
}
|
|
||||||
if previous.Y == "" {
|
|
||||||
previous.Y = obj.TextCode[i].Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if previous.X == "" {
|
|
||||||
previous.X = "0"
|
|
||||||
}
|
|
||||||
if previous.Y == "" {
|
|
||||||
previous.Y = "0"
|
|
||||||
}
|
|
||||||
if code.X == "" {
|
|
||||||
code.X = previous.X
|
|
||||||
}
|
|
||||||
startX := obj.TextCode[0].X
|
|
||||||
if startX == "" {
|
|
||||||
startX = "0"
|
|
||||||
}
|
|
||||||
x, ex := strconv.ParseFloat(code.X, 64)
|
|
||||||
start, es := strconv.ParseFloat(startX, 64)
|
|
||||||
y, ey := strconv.ParseFloat(code.Y, 64)
|
|
||||||
prev, ep := strconv.ParseFloat(previous.Y, 64)
|
|
||||||
if ex != nil || es != nil || ey != nil || ep != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if x == start && y != prev {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
left, err := strconv.ParseFloat(previous.X, 64)
|
|
||||||
if err != nil || obj.Size <= 0 || y-prev < obj.Size/2 || previous.DeltaY != "" || code.DeltaY != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
count := obj.textCodeGlyphCount(index - 1)
|
|
||||||
if count == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
right := left + obj.Size
|
|
||||||
positioner := NewTextPositioner(previous)
|
|
||||||
for range count {
|
|
||||||
right = math.Max(right, positioner.Next().X+obj.Size)
|
|
||||||
}
|
|
||||||
return x <= right
|
|
||||||
}
|
|
||||||
|
|
||||||
// textCodeGlyphCount 统计文本段实际绘制的字形数量,计入字符到字形的变换
|
|
||||||
// 入参: index 文本段索引
|
|
||||||
// 返回: int 字形数量
|
|
||||||
func (obj TextObject) textCodeGlyphCount(index int) int {
|
|
||||||
code := obj.TextCode[index]
|
|
||||||
if code.Index != "" {
|
|
||||||
count := 0
|
|
||||||
for _, field := range strings.Fields(code.Index) {
|
|
||||||
first, last, interval := strings.Cut(field, "-")
|
|
||||||
start, err := strconv.ParseUint(first, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !interval {
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
end, err := strconv.ParseUint(last, 10, 16)
|
|
||||||
if err == nil && end >= start {
|
|
||||||
count += int(end-start) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
count := textCodeRuneCount(code.Value)
|
|
||||||
if len(obj.CGTransform) == 0 || count == 0 {
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
offset := 0
|
|
||||||
for _, previous := range obj.TextCode[:index] {
|
|
||||||
offset += textCodeRuneCount(previous.Value)
|
|
||||||
}
|
|
||||||
transforms := make(map[int][2]int)
|
|
||||||
for _, transform := range obj.CGTransform {
|
|
||||||
start, codes := transform.CodePosition-offset, max(1, transform.CodeCount)
|
|
||||||
if start < 0 || start >= count {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
glyphs := len(parseInts(transform.Glyphs))
|
|
||||||
if transform.GlyphCount > 0 {
|
|
||||||
glyphs = min(glyphs, transform.GlyphCount)
|
|
||||||
}
|
|
||||||
if glyphs > 0 {
|
|
||||||
transforms[start] = [2]int{codes, glyphs}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
glyphs := 0
|
|
||||||
for i := 0; i < count; {
|
|
||||||
if transform, ok := transforms[i]; ok && transform[0] <= count-i {
|
|
||||||
i += transform[0]
|
|
||||||
glyphs += transform[1]
|
|
||||||
} else {
|
|
||||||
i++
|
|
||||||
glyphs++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return glyphs
|
|
||||||
}
|
|
||||||
|
|
||||||
// textCodeRuneCount 统计文本编码中的字符数,识别转义及XML排版空白
|
// textCodeRuneCount 统计文本编码中的字符数,识别转义及XML排版空白
|
||||||
// 入参: value 文本编码内容
|
// 入参: value 文本编码内容
|
||||||
// 返回: int 字符数量
|
// 返回: int 字符数量
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// 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 (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// textLineScanner 顺序判断文本段换行,复用继承坐标和字符到字形的变换
|
||||||
|
type textLineScanner struct {
|
||||||
|
size float64
|
||||||
|
enabled bool
|
||||||
|
started bool
|
||||||
|
firstX string
|
||||||
|
previous TextCode
|
||||||
|
offset int
|
||||||
|
pending []CGTransform
|
||||||
|
transforms map[int][2]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTextLineScanner 创建独立的换行扫描器,不修改文字对象
|
||||||
|
// 入参: obj 定位文字
|
||||||
|
// 返回: textLineScanner 扫描状态
|
||||||
|
func newTextLineScanner(obj TextObject) textLineScanner {
|
||||||
|
return textLineScanner{size: obj.Size, enabled: len(obj.TextCode) > 1 && obj.ReadDirection == 0 && obj.CharDirection == 0, previous: TextCode{X: "0", Y: "0"}, pending: obj.CGTransform}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glyphCount 首次需要字形范围时解析变换,后续文本段复用结果
|
||||||
|
// 入参: code 文本段, offset 对象内字符起点
|
||||||
|
// 返回: int 字形数量
|
||||||
|
func (s *textLineScanner) glyphCount(code TextCode, offset int) int {
|
||||||
|
if len(s.pending) != 0 {
|
||||||
|
s.transforms = make(map[int][2]int, len(s.pending))
|
||||||
|
}
|
||||||
|
for _, transform := range s.pending {
|
||||||
|
if transform.CodePosition < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
glyphs := len(parseInts(transform.Glyphs))
|
||||||
|
if transform.GlyphCount > 0 {
|
||||||
|
glyphs = min(glyphs, transform.GlyphCount)
|
||||||
|
}
|
||||||
|
if glyphs > 0 {
|
||||||
|
s.transforms[transform.CodePosition] = [2]int{max(1, transform.CodeCount), glyphs}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.pending = nil
|
||||||
|
return textCodeGlyphCount(code, offset, s.transforms)
|
||||||
|
}
|
||||||
|
|
||||||
|
// next 继承文本段原点并根据实际字形范围判断是否换行
|
||||||
|
// 入参: code 按文档顺序传入的文本段
|
||||||
|
// 返回: bool 当前段前是否换行
|
||||||
|
func (s *textLineScanner) next(code TextCode) bool {
|
||||||
|
if !s.enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
previous, offset := s.previous, s.offset
|
||||||
|
if s.started && (len(s.pending) != 0 || len(s.transforms) != 0) {
|
||||||
|
s.offset += textCodeRuneCount(previous.Value)
|
||||||
|
}
|
||||||
|
if code.X == "" {
|
||||||
|
code.X = previous.X
|
||||||
|
}
|
||||||
|
if code.Y == "" {
|
||||||
|
code.Y = previous.Y
|
||||||
|
}
|
||||||
|
s.previous = code
|
||||||
|
if !s.started {
|
||||||
|
s.started, s.firstX = true, code.X
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
x, ex := strconv.ParseFloat(code.X, 64)
|
||||||
|
start, es := strconv.ParseFloat(s.firstX, 64)
|
||||||
|
y, ey := strconv.ParseFloat(code.Y, 64)
|
||||||
|
prev, ep := strconv.ParseFloat(previous.Y, 64)
|
||||||
|
if ex != nil || es != nil || ey != nil || ep != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if x == start && y != prev {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
left, err := strconv.ParseFloat(previous.X, 64)
|
||||||
|
if err != nil || s.size <= 0 || y-prev < s.size/2 || previous.DeltaY != "" || code.DeltaY != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
count := s.glyphCount(previous, offset)
|
||||||
|
if count == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
right := left + s.size
|
||||||
|
positioner := NewTextPositioner(previous)
|
||||||
|
for range count {
|
||||||
|
right = math.Max(right, positioner.Next().X+s.size)
|
||||||
|
}
|
||||||
|
return x <= right
|
||||||
|
}
|
||||||
|
|
||||||
|
// textCodeGlyphCount 按文本段的字符起点统计实际字形数量
|
||||||
|
// 入参: code 文本段, offset 对象内字符起点, transforms 已解析的字符到字形变换
|
||||||
|
// 返回: int 字形数量
|
||||||
|
func textCodeGlyphCount(code TextCode, offset int, transforms map[int][2]int) int {
|
||||||
|
if code.Index != "" {
|
||||||
|
count := 0
|
||||||
|
for _, field := range strings.Fields(code.Index) {
|
||||||
|
first, last, interval := strings.Cut(field, "-")
|
||||||
|
start, err := strconv.ParseUint(first, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !interval {
|
||||||
|
count++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseUint(last, 10, 16)
|
||||||
|
if err == nil && end >= start {
|
||||||
|
count += int(end-start) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
count := textCodeRuneCount(code.Value)
|
||||||
|
if len(transforms) == 0 || count == 0 {
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
glyphs := 0
|
||||||
|
for i := 0; i < count; {
|
||||||
|
if transform, ok := transforms[offset+i]; ok && transform[0] <= count-i {
|
||||||
|
i += transform[0]
|
||||||
|
glyphs += transform[1]
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
glyphs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return glyphs
|
||||||
|
}
|
||||||
@@ -85,6 +85,12 @@ func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement
|
|||||||
if attr.Name.Local == "LineWidth" {
|
if attr.Name.Local == "LineWidth" {
|
||||||
obj.LineWidthSet = obj.LineWidth == 0
|
obj.LineWidthSet = obj.LineWidth == 0
|
||||||
}
|
}
|
||||||
|
if attr.Name.Local == "MiterLimit" {
|
||||||
|
obj.MiterLimitSet = obj.MiterLimit == 0
|
||||||
|
}
|
||||||
|
if attr.Name.Local == "DashPattern" {
|
||||||
|
obj.dashPatternSet = obj.DashPattern == ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+20
-10
@@ -122,7 +122,7 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
|
|||||||
record := func(node *editorXML, attribute, target string) {
|
record := func(node *editorXML, attribute, target string) {
|
||||||
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
|
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
|
||||||
}
|
}
|
||||||
if err := view.scanDocumentFileReferences(ctx, name, false, add, record); err != nil {
|
if err := view.scanDocumentFileReferences(ctx, name, false, add, record, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
|
|||||||
shared[file.Location] = true
|
shared[file.Location] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.versionInfo != nil {
|
if r.versionInfo != nil && r.OFD.DocBody[r.documentIndex].DocRoot != "" {
|
||||||
entry, err := versionFileLocation("OFD.xml", r.OFD.DocBody[r.documentIndex].DocRoot)
|
entry, err := versionFileLocation("OFD.xml", r.OFD.DocBody[r.documentIndex].DocRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -238,9 +238,9 @@ func (r *Reader) versionChanges(ctx context.Context, changes map[string][]byte)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// versionFileUnchanged 分块比较原文件与待写入内容,读取失败时保留替换内容
|
// versionFileUnchanged 分块比较原文件与待写入内容,读取失败时保留替换内容
|
||||||
// 入参: ctx 取消上下文, name 包内路径, data 待写入内容, buffer 非空比较缓冲区
|
// 入参: ctx 取消上下文, name 包内路径, data 待写入内容, buffer 复用缓冲区,比较ZIP条目时按需分配
|
||||||
// 返回: bool 内容相同, error 取消错误
|
// 返回: bool 内容相同, error 取消错误
|
||||||
func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, buffer []byte) (bool, error) {
|
func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data []byte, buffer *[]byte) (bool, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
end := offset + min(len(buffer), len(data)-offset)
|
end := offset + min(32*1024, len(data)-offset)
|
||||||
if !bytes.Equal(original[offset:end], data[offset:end]) {
|
if !bytes.Equal(original[offset:end], data[offset:end]) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -270,9 +270,12 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
defer input.Close()
|
defer input.Close()
|
||||||
|
if len(*buffer) == 0 {
|
||||||
|
*buffer = make([]byte, 32*1024)
|
||||||
|
}
|
||||||
reader := imageInput{ReadCloser: input, context: ctx}
|
reader := imageInput{ReadCloser: input, context: ctx}
|
||||||
for len(data) != 0 {
|
for len(data) != 0 {
|
||||||
block := buffer[:min(len(buffer), len(data))]
|
block := (*buffer)[:min(len(*buffer), len(data))]
|
||||||
if _, err := io.ReadFull(&reader, block); err != nil {
|
if _, err := io.ReadFull(&reader, block); err != nil {
|
||||||
return false, ctx.Err()
|
return false, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -281,11 +284,11 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
|
|||||||
}
|
}
|
||||||
data = data[len(block):]
|
data = data[len(block):]
|
||||||
}
|
}
|
||||||
n, err := reader.Read(buffer[:1])
|
n, err := reader.Read((*buffer)[:1])
|
||||||
return n == 0 && err == io.EOF, ctx.Err()
|
return n == 0 && err == io.EOF, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// versionOutputChanges 隔离文档或版本改动并同步删除清单,保留其他入口仍需使用的文件
|
// versionOutputChanges 隔离文档或版本改动并同步删除清单,失败时不修改调用方清单
|
||||||
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
|
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
|
||||||
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
|
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
|
||||||
func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]byte, removed map[string]bool, generated map[string]editorGeneratedReferences) (map[string][]byte, error) {
|
func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]byte, removed map[string]bool, generated map[string]editorGeneratedReferences) (map[string][]byte, error) {
|
||||||
@@ -303,12 +306,12 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
if parts == nil {
|
if parts == nil {
|
||||||
parts = make(map[string][]byte)
|
parts = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
buffer := make([]byte, 32*1024)
|
var buffer []byte
|
||||||
for name, data := range parts {
|
for name, data := range parts {
|
||||||
if generated[name].stagedLeaf(data) {
|
if generated[name].stagedLeaf(data) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
same, err := r.versionFileUnchanged(ctx, name, data, buffer)
|
same, err := r.versionFileUnchanged(ctx, name, data, &buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -339,6 +342,8 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
outputRemoved := removed
|
||||||
|
removed = maps.Clone(removed)
|
||||||
for name := range removed {
|
for name := range removed {
|
||||||
if graph.used[name] {
|
if graph.used[name] {
|
||||||
delete(removed, name)
|
delete(removed, name)
|
||||||
@@ -434,6 +439,11 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for name := range outputRemoved {
|
||||||
|
if _, ok := removed[name]; !ok {
|
||||||
|
delete(outputRemoved, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
for source, target := range paths {
|
for source, target := range paths {
|
||||||
if known := generated[source]; known.stagedLeaf(parts[target]) {
|
if known := generated[source]; known.stagedLeaf(parts[target]) {
|
||||||
generated[target] = known
|
generated[target] = known
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
body := root.childAt("DocBody", r.documentIndex)
|
body := root.childAt("DocBody", r.documentIndex)
|
||||||
if body == nil || body.child("DocRoot") == nil {
|
if body == nil {
|
||||||
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
||||||
}
|
}
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
@@ -133,9 +133,17 @@ func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry := body.child("DocRoot")
|
entry := body.child("DocRoot")
|
||||||
|
if entry != nil {
|
||||||
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
|
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
|
||||||
|
}
|
||||||
if versions := body.child("Versions"); versions != nil {
|
if versions := body.child("Versions"); versions != nil {
|
||||||
patches = append(patches, editorXMLPatch{start: versions.start, end: versions.end})
|
var replacement []byte
|
||||||
|
if entry == nil {
|
||||||
|
replacement = body.textXML("DocRoot", version.DocRoot)
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{versions.start, versions.end, replacement})
|
||||||
|
} else if entry == nil {
|
||||||
|
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
||||||
}
|
}
|
||||||
references := []*editorXML{body.child("Signatures")}
|
references := []*editorXML{body.child("Signatures")}
|
||||||
if info := body.child("DocInfo"); info != nil {
|
if info := body.child("DocInfo"); info != nil {
|
||||||
|
|||||||
+10
-2
@@ -104,11 +104,19 @@ func (r *Reader) DocumentVersionID() (string, error) {
|
|||||||
// 入参: id 版本标识,空值显式选择主入口;Document可重新按Current选择默认版本
|
// 入参: id 版本标识,空值显式选择主入口;Document可重新按Current选择默认版本
|
||||||
// 返回: *Reader 版本阅读器, error 选择或读取错误
|
// 返回: *Reader 版本阅读器, error 选择或读取错误
|
||||||
func (r *Reader) DocumentVersion(id string) (*Reader, error) {
|
func (r *Reader) DocumentVersion(id string) (*Reader, error) {
|
||||||
|
return r.DocumentVersionContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentVersionContext 创建指定版本的独立阅读器,读取版本及关联内容时响应取消
|
||||||
|
// 返回值不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
||||||
|
// 入参: ctx 取消上下文, id 版本标识,空值选择主入口
|
||||||
|
// 返回: *Reader 版本阅读器, error 读取或取消错误
|
||||||
|
func (r *Reader) DocumentVersionContext(ctx context.Context, id string) (*Reader, error) {
|
||||||
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id}
|
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id}
|
||||||
if err := next.initRoot(); err != nil {
|
if err := next.initRootContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := next.Doc(); err != nil {
|
if _, err := next.DocContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return next, nil
|
return next, nil
|
||||||
|
|||||||
+56
-9
@@ -141,6 +141,14 @@ func (e *Editor) Reader() (*Reader, error) {
|
|||||||
return e.reader(nil)
|
return e.reader(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReaderContext 获取可取消的独立内存快照,失败时不返回部分结果
|
||||||
|
// 快照保持Reader的资源所有权规则,不改变当前编辑状态或撤销记录
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *Reader 阅读器, error 准备或取消错误
|
||||||
|
func (e *Editor) ReaderContext(ctx context.Context) (*Reader, error) {
|
||||||
|
return e.readerContext(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// Write 写入并累计字节数
|
// Write 写入并累计字节数
|
||||||
// 入参: data 数据
|
// 入参: data 数据
|
||||||
// 返回: int 字节数, error 错误信息
|
// 返回: int 字节数, error 错误信息
|
||||||
@@ -276,11 +284,11 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
|
|||||||
if err := r.initRootContext(ctx); err != nil {
|
if err := r.initRootContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := r.Doc(); err != nil {
|
if _, err := r.DocContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i, page := range r.doc.Pages.Page {
|
for i, page := range r.doc.Pages.Page {
|
||||||
r.pageHeaderCache[r.ResPath(page.BaseLoc)] = PageContent{Area: PageArea{PhysicalBox: e.pages[i].Area.PhysicalBox}}
|
r.pageHeaderCache[r.ResPath(page.BaseLoc)] = PageContent{Area: e.pages[i].Area, Template: slices.Clone(e.pages[i].Template), PageRes: slices.Clone(e.pages[i].PageRes)}
|
||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
@@ -411,9 +419,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
x.root("Document", nil)
|
x.root("Document", nil)
|
||||||
x.start("CommonData", nil)
|
x.start("CommonData", nil)
|
||||||
x.text("MaxUnitID", strconv.Itoa(e.maxID))
|
x.text("MaxUnitID", strconv.Itoa(e.maxID))
|
||||||
x.start("PageArea", nil)
|
x.pageArea("PageArea", e.pages[0].Area)
|
||||||
x.text("PhysicalBox", e.pages[0].Area.PhysicalBox)
|
|
||||||
x.end("PageArea")
|
|
||||||
if len(fonts)+len(images)+len(spaces) != 0 {
|
if len(fonts)+len(images)+len(spaces) != 0 {
|
||||||
x.text("DocumentRes", "DocumentRes.xml")
|
x.text("DocumentRes", "DocumentRes.xml")
|
||||||
}
|
}
|
||||||
@@ -620,13 +626,11 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
return fonts, images, spaces, files
|
return fonts, images, spaces, files
|
||||||
}
|
}
|
||||||
|
|
||||||
// page 写出页面尺寸、图层和对象
|
// page 按标准顺序写出页面定义、图层、对象及动作
|
||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
func (x *ofdXML) page(page PageContent) {
|
func (x *ofdXML) page(page PageContent) {
|
||||||
x.root("Page", nil)
|
x.root("Page", nil)
|
||||||
x.start("Area", nil)
|
x.pageHeader(page)
|
||||||
x.text("PhysicalBox", page.Area.PhysicalBox)
|
|
||||||
x.end("Area")
|
|
||||||
x.start("Content", nil)
|
x.start("Content", nil)
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
if x.err != nil {
|
if x.err != nil {
|
||||||
@@ -639,6 +643,34 @@ func (x *ofdXML) page(page PageContent) {
|
|||||||
x.end("Page")
|
x.end("Page")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pageHeader 写出模板、页资源及页面区域
|
||||||
|
// 入参: page 页面内容
|
||||||
|
func (x *ofdXML) pageHeader(page PageContent) {
|
||||||
|
for _, template := range page.Template {
|
||||||
|
attrs := ofdAttrs{{Name: xml.Name{Local: "TemplateID"}, Value: template.TemplateID}}
|
||||||
|
attrs.add("ZOrder", template.ZOrder)
|
||||||
|
x.start("Template", attrs)
|
||||||
|
x.end("Template")
|
||||||
|
}
|
||||||
|
for _, resource := range page.PageRes {
|
||||||
|
x.text("PageRes", resource)
|
||||||
|
}
|
||||||
|
x.pageArea("Area", page.Area)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageArea 写出物理、显示、版心及出血区域,省略未定义的可选区域
|
||||||
|
// 入参: name 区域容器名称, area 页面区域
|
||||||
|
func (x *ofdXML) pageArea(name string, area PageArea) {
|
||||||
|
x.start(name, nil)
|
||||||
|
x.text("PhysicalBox", area.PhysicalBox)
|
||||||
|
for _, field := range [][2]string{{"ApplicationBox", area.ApplicationBox}, {"ContentBox", area.ContentBox}, {"BleedBox", area.BleedBox}} {
|
||||||
|
if field[1] != "" {
|
||||||
|
x.text(field[0], field[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.end(name)
|
||||||
|
}
|
||||||
|
|
||||||
// layer 写出新建图层及其对象
|
// layer 写出新建图层及其对象
|
||||||
// 入参: layer 图层
|
// 入参: layer 图层
|
||||||
func (x *ofdXML) layer(layer Layer) {
|
func (x *ofdXML) layer(layer Layer) {
|
||||||
@@ -890,7 +922,19 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("LineWidth", "0")
|
attrs.add("LineWidth", "0")
|
||||||
}
|
}
|
||||||
attrs.number("MiterLimit", obj.MiterLimit)
|
attrs.number("MiterLimit", obj.MiterLimit)
|
||||||
|
if obj.MiterLimitSet && obj.MiterLimit == 0 {
|
||||||
|
attrs.add("MiterLimit", "0")
|
||||||
|
}
|
||||||
attrs.add("Join", obj.Join)
|
attrs.add("Join", obj.Join)
|
||||||
|
attrs.add("Cap", obj.Cap)
|
||||||
|
if obj.dashPatternSet {
|
||||||
|
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: obj.DashPattern})
|
||||||
|
} else {
|
||||||
|
attrs.add("DashPattern", obj.DashPattern)
|
||||||
|
}
|
||||||
|
if obj.DashOffset != nil {
|
||||||
|
attrs.add("DashOffset", ofdNumber(*obj.DashOffset))
|
||||||
|
}
|
||||||
if obj.Italic {
|
if obj.Italic {
|
||||||
attrs.add("Italic", "true")
|
attrs.add("Italic", "true")
|
||||||
}
|
}
|
||||||
@@ -911,6 +955,9 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
|||||||
attrs.add("LineWidth", "0")
|
attrs.add("LineWidth", "0")
|
||||||
}
|
}
|
||||||
attrs.number("MiterLimit", obj.MiterLimit)
|
attrs.number("MiterLimit", obj.MiterLimit)
|
||||||
|
if obj.MiterLimitSet && obj.MiterLimit == 0 {
|
||||||
|
attrs.add("MiterLimit", "0")
|
||||||
|
}
|
||||||
attrs.add("Join", obj.Join)
|
attrs.add("Join", obj.Join)
|
||||||
attrs.add("Cap", obj.Cap)
|
attrs.add("Cap", obj.Cap)
|
||||||
attrs.add("Rule", obj.Rule)
|
attrs.add("Rule", obj.Rule)
|
||||||
|
|||||||
Reference in new issue
Block a user