feat(渲染编辑): 完善渲染编辑,优化版本压缩
build.yaml / build (push) Successful in 4m25s

This commit is contained in:
xiaoqidun committed 2026-10-12 10:47:20 +08:00
1 parent 19dc79e8e3
commit a710bc5f41
68 files changed
+2063 -638

No files matched your search

+3 -3
View File
@@ -2225,7 +2225,7 @@ function updateVersionControls() {
el.versionList.inert = busy || Boolean(manager?.loading);
el.versionName.disabled = el.versionAdd.disabled = unavailable || !state.editing;
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.versionSelect.disabled = unavailable || entry?.id === (state.doc?.documentVersion || "");
}
@@ -2695,7 +2695,7 @@ async function editCanvasObject(item) {
}
if (item.type === "TextObject") {
if (!canEditObject(item, "textContent")) {
if (!item.scoped && canEditObject(item, "rewriteText")) {
if (canEditObject(item, "rewriteText")) {
state.sourceText = { item, openSeq: state.openSeq };
el.sourceTextValue.value = item.text;
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.objectDistribute.disabled = el.objectAlign.disabled || !item.items || item.items.length < 3;
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.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
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/image v0.47.0
golang.org/x/text v0.43.0
+2 -2
View File
@@ -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/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
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-20261011164142-f98f64ee48bb/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
github.com/xiaoqidun/pdfgo v0.0.0-20261012024528-667f6fbbcb3f h1:5fYd+Fv3MoSeS5cOEsSU7y4Oo2IXpDn14e1P/rZTvlY=
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+67 -34
View File
@@ -1869,6 +1869,12 @@ func editorAppearance(item map[string]any, object ofdgo.GraphicObject, paint boo
if path.DashOffset != nil {
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 {
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))
@@ -2259,36 +2265,40 @@ func editorFont(args []js.Value) (any, error) {
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 生成保留原定位的输入预览,不修改文档或历史
// 入参: args 页码、对象路径、新文字、可选字体数据和字号
// 返回: any 定位段及字体样式, error 错误信息
func previewText(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
page, key := args[0].Int(), args[1].String()
var object ofdgo.GraphicObject
if separator := strings.LastIndex(key, "/"); separator >= 0 {
path, err := compositePath(key[:separator])
key := args[1].String()
object, err := editorObjectAt(args[0].Int(), key)
if err != nil {
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
if object.Type != "TextObject" {
return nil, fmt.Errorf("object is not text")
@@ -2361,10 +2371,10 @@ func previewText(args []js.Value) (any, error) {
}
// previewPositionedText 预览保留定位的文字替换,不修改编辑器或撤销记录
// 入参: args 页面、对象标识和内容
// 入参: args 页面、对象路径和内容
// 返回: any SVG预览, 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 {
return nil, err
}
@@ -3190,6 +3200,24 @@ func resolveStylePaint(paint *ofdgo.FillColor) error {
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 读取高级面板提交的外观字段,省略字段保持原值
// 入参: value 前端样式
// 返回: ofdgo.ObjectStyle 库层样式
@@ -3453,21 +3481,26 @@ func documentVersions(args []js.Value) (any, error) {
if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened")
}
var versions []ofdgo.DocumentVersionInfo
var err error
reader := currentSession.Reader
if currentEditor != nil {
versions, err = currentEditor.DocumentVersions()
} else {
versions, err = currentSession.Reader.DocumentVersions()
}
var err error
reader, err = currentEditor.Reader()
if err != nil {
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 {
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
}
}
+32 -8
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"context"
"encoding/xml"
"fmt"
)
@@ -149,13 +150,13 @@ func (r *Reader) annotationInfos(index int, pageID string) []AnnotationInfo {
}
// parseAnnotations 解析注释文件
// 入参: doc 文档结构
// 入参: ctx 取消上下文, doc 文档结构
// 返回: error 错误信息
func (r *Reader) parseAnnotations(doc *Document) error {
func (r *Reader) parseAnnotations(ctx context.Context, doc *Document) error {
if doc.Annotations == "" {
return nil
}
annotations, err := r.annotationIndex(doc)
annotations, err := r.annotationIndexContext(ctx, doc)
if err != nil {
return err
}
@@ -164,26 +165,39 @@ func (r *Reader) parseAnnotations(doc *Document) error {
}
annPath := r.ResPath(doc.Annotations)
for _, page := range annotations.Page {
if err := ctx.Err(); err != nil {
return err
}
annotPath := resolveResourcePath(annPath, "", page.FileLoc)
af, err := r.openFile(annotPath)
if err != nil {
continue
}
var pageAnnot PageAnnot
err = xml.NewDecoder(af).Decode(&pageAnnot)
err = xml.NewDecoder(imageInput{ReadCloser: af, context: ctx}).Decode(&pageAnnot)
_ = af.Close()
if err != nil {
continue
}
r.Annots[page.PageID] = append(r.Annots[page.PageID], pageAnnot.Annot...)
}
return nil
return ctx.Err()
}
// annotationIndex 读取注解索引,将页引用对应到文档声明的标识,不解析页面外观
// 入参: doc 文档结构
// 返回: 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 == "" {
return Annotations{}, nil
}
@@ -194,17 +208,23 @@ func (r *Reader) annotationIndex(doc *Document) (Annotations, error) {
}
defer f.Close()
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
}
pageIDs := make(map[string]string, len(doc.Pages.Page))
for _, page := range doc.Pages.Page {
if err := ctx.Err(); err != nil {
return Annotations{}, err
}
if id := editorResourceID(page.ID); id != "" {
pageIDs[id] = page.ID
}
}
r.annotationFiles = make(map[string][]string)
files := make(map[string][]string)
for i := range annotations.Page {
if err := ctx.Err(); err != nil {
return Annotations{}, err
}
page := &annotations.Page[i]
if id := pageIDs[editorResourceID(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 {
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
}
+9 -18
View File
@@ -140,7 +140,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
return &snapshot, nil
}
if e.source != nil {
protected, err := e.source.reader.hasOutputSignatures()
protected, err := e.source.reader.hasOutputSignatures(ctx)
if err != nil {
return nil, err
}
@@ -285,28 +285,19 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
return paths, safe, ctx.Err()
}
// hasOutputSignatures 检查包中各文档,避免改写其他文档签名保护的共享资源
// 返回: bool 是否存在签名, error 文档读取错误
func (r *Reader) hasOutputSignatures() (bool, error) {
// hasOutputSignatures 检查全部文档的签名入口,不受当前文档或版本选择影响
// 入参: ctx 取消上下文
// 返回: bool 是否声明签名入口, error 取消错误
func (r *Reader) hasOutputSignatures(ctx context.Context) (bool, error) {
for _, body := range r.OFD.DocBody {
if err := ctx.Err(); err != nil {
return false, err
}
if body.Signatures != "" {
return true, nil
}
data, err := r.readFile(body.DocRoot)
if err != nil {
return false, err
}
var doc struct {
Signatures string `xml:"Signatures"`
}
if err := xml.Unmarshal(data, &doc); err != nil {
return false, err
}
if doc.Signatures != "" {
return true, nil
}
}
return false, nil
return false, ctx.Err()
}
// writeOutputEntry 优化ZIP编码,保留资源名称及条目的未压缩字节
+5 -4
View File
@@ -49,7 +49,7 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
document := r
if index != r.DocumentIndex() {
var err error
document, err = r.Document(index)
document, err = r.DocumentContext(ctx, index)
if err != nil {
return nil, nil, err
}
@@ -112,18 +112,19 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
if dpi == 0 {
return nil, nil
}
count, err := r.PageCount()
doc, err := r.DocContext(ctx)
if err != nil {
return nil, ctx.Err()
}
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
renderer.pageContext = ctx
defer renderer.ClearCache()
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 {
return nil, err
}
page, err := r.PageContentByIndex(index)
page, err := r.PageContentContext(ctx, ref)
if err != nil {
return nil, ctx.Err()
}
+19 -21
View File
@@ -15,11 +15,8 @@
package ofdgo
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"maps"
"slices"
)
@@ -40,37 +37,38 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
seen := make(map[[32]byte]string)
aliases := make(map[string]string)
indexes := make(map[string]map[string]string)
buffer := make([]byte, 32*1024)
for _, name := range slices.Sorted(maps.Keys(profiles)) {
ordered := 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 {
return err
}
if protected[name] {
continue
}
var input io.ReadCloser
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)
size, err := outputResourceSize(reader, parts, name)
if err != nil {
return err
}
sizes[name] = size
counts[size]++
}
hash := sha256.New()
_, err := io.CopyBuffer(hash, imageInput{ReadCloser: input, context: ctx}, buffer)
closeErr := input.Close()
var buffer []byte
for _, name := range ordered {
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 {
return err
}
if closeErr != nil {
return closeErr
}
key := [32]byte(hash.Sum(nil))
first, found := seen[key]
if !found {
seen[key] = name
+103 -16
View File
@@ -16,10 +16,12 @@ package ofdgo
import (
"bytes"
"context"
"crypto/sha256"
"encoding/xml"
"fmt"
"image"
"io"
"maps"
"path"
"slices"
@@ -105,6 +107,10 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
return err
}
type resourceFile struct{ key, role string }
type resourceSize struct {
role string
size uint64
}
files := make(map[resourceFile]bool)
for _, name := range resources {
data, err := read(name)
@@ -171,31 +177,43 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
imageResults := make(map[outputImageKey][]byte)
imageSources := make(map[string]outputImageSource)
ordered := slices.Sorted(maps.Keys(actual))
sizes := make(map[string]uint64, len(files))
largeCounts := make(map[resourceSize]int)
var imageKeys []string
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)
}
}
load := func(key string) ([]byte, error) {
if data, ok := parts[actual[key]]; ok && len(data) > outputOptimizationBufferLimit {
if sizes[key] > outputOptimizationBufferLimit {
return nil, nil
}
if reader != nil && parts[actual[key]] == nil {
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
return read(actual[key])
}
if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil {
return err
}
var fontCache editorSourceFontCache
var hashBuffer []byte
for _, key := range ordered {
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
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 {
return err
}
if size := sizes[key]; size > outputOptimizationBufferLimit && largeCounts[resourceSize{role, size}] < 2 {
continue
}
source, prefetched := imageSources[key]
usage := fontUsage[key]
subset := role == "FontFile" && usage != nil && !usage.unsafe && e.backends.FontResources != nil
var data []byte
optimizable := prefetched
if !prefetched {
var err error
if role == "MediaFile" || subset {
data, err = load(key)
if err != nil {
return err
}
if data == nil {
continue
}
optimizable = data != nil
if optimizable {
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)
if err != nil {
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)
}
}
if role == "MediaFile" {
if role == "MediaFile" && optimizable {
options, demand := e.outputImageOptions(key)
cacheKey := outputImageKey{hash: source.key.hash, options: options, size: demand}
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 {
if refs.versions[cleanPackagePath(name)] {
continue
@@ -357,3 +394,53 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
}
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
View File
@@ -21,6 +21,7 @@ import (
)
// Document 文档结构
// Signatures由阅读器按主入口和所选版本解析,不属于文档根节点的XML内容
type Document struct {
XMLName xml.Name `xml:"Document"`
CommonData CommonData `xml:"CommonData"`
@@ -30,7 +31,7 @@ type Document struct {
Actions []Action `xml:"Actions>Action"`
Bookmarks Bookmarks `xml:"Bookmarks"`
Annotations string `xml:"Annotations"`
Signatures string `xml:"Signatures"`
Signatures string `xml:"-"`
Attachments Attachments `xml:"Attachments"`
CustomTags CustomTags `xml:"CustomTags"`
Extensions Extensions `xml:"Extensions"`
+2 -1
View File
@@ -78,8 +78,9 @@ func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
if dp.DashOffset != nil {
base.DashOffset = dp.DashOffset
}
if dp.MiterLimit > 0 {
if dp.MiterLimit > 0 || dp.MiterLimitSet {
base.MiterLimit = dp.MiterLimit
base.MiterLimitSet = dp.MiterLimitSet
}
if dp.FillColor != nil {
base.FillColor = dp.FillColor
+26 -4
View File
@@ -1045,6 +1045,11 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
obj.LineWidth = defaultPathLineWidth
}
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 {
code := &obj.TextCode[i]
for _, value := range []*string{&code.X, &code.Y, &code.DeltaX, &code.DeltaY} {
@@ -1099,6 +1104,9 @@ func scaleTextNumbers(value string, scale float64) string {
// 入参: obj 文字对象
// 返回: error 错误信息
func (e *Editor) prepareText(obj *TextObject) error {
if err := validateEditorStroke(obj.pathStyle()); err != nil {
return err
}
if id := editorResourceID(obj.Font); id != "" {
obj.Font = id
}
@@ -1133,7 +1141,7 @@ func (e *Editor) prepareText(obj *TextObject) error {
if err := validateTextGlyphs(*obj, glyphCount); err != nil {
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")
}
if obj.Weight < 0 || obj.Weight > 900 || obj.Weight%100 != 0 {
@@ -1294,10 +1302,24 @@ func creationBox(value string) (Box, error) {
if err != nil {
return Box{}, err
}
if values[2] <= 0 || values[3] <= 0 {
return Box{}, fmt.Errorf("boundary dimensions must be positive")
box := Box{X: values[0], Y: values[1], W: values[2], H: values[3]}
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颜色空间中的基本颜色
+31 -8
View File
@@ -184,7 +184,7 @@ func (e *Editor) FlipCompositeObjects(page int, path ObjectPath, indexes []int,
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, 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 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 错误信息
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 {
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 {
capability := members[i].Capabilities
if !capability.Transform || paint && !capability.Paint {
return fmt.Errorf("composite member %d cannot accept this style: %w", indexes[i], capability.editError())
}
local := style
var painted PathObject
switch members[i].Object.Type {
case "TextObject":
local.FillColor = editorStyleColor(style.FillColor, members[i].Object.TextObject.FillColor)
painted = members[i].Object.TextObject.pathStyle()
case "PathObject":
p := members[i].Object.PathObject
local.FillColor = editorStyleColor(style.FillColor, p.FillColor)
local.StrokeColor = (*StrokeColor)(editorStyleColor((*FillColor)(style.StrokeColor), (*FillColor)(p.StrokeColor)))
painted = members[i].Object.PathObject
}
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 {
width := *style.LineWidth / members[i].StrokeScale
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)
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 {
clips := node.object.ImageObject.Clips
_, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert()
+1 -1
View File
@@ -27,7 +27,7 @@ import (
// 入参: page 页面索引, path 父复合路径, index 图片序号, 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 e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
+149 -1
View File
@@ -179,6 +179,9 @@ func (e *Editor) wrapCompositePath(renderer *Renderer, node *editorCompositeNode
// 入参: node 页面坐标中的对象节点
// 返回: 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" {
id, err := e.neutralDrawParam()
if err != nil {
@@ -220,7 +223,149 @@ func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error {
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 错误信息
func (e *Editor) neutralDrawParam() (string, error) {
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.number("MiterLimit", draw.MiterLimit)
if draw.MiterLimitSet && draw.MiterLimit == 0 {
attrs.add("MiterLimit", "0")
}
if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" {
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern})
}
+1 -1
View File
@@ -312,7 +312,7 @@ func (e *Editor) reshapeCompositeObject(page int, path ObjectPath, index int, ki
if lineShapeKind(kind) {
object.PathObject, err = object.PathObject.ReshapeLine(kind, transformLineBox(box, inverse))
} else {
if _, err := creationBox(editorBoxString(box)); err != nil {
if err := validateCreationBox(box); err != nil {
return err
}
object.PathObject, err = object.PathObject.Reshape(inverse.TransformBox(box))
+17 -4
View File
@@ -16,19 +16,21 @@ package ofdgo
import "fmt"
// Style 获取内部成员的独立样式快照,路径CTM只表达页面描边倍率
// Style 获取内部成员的独立样式快照,文字和路径CTM只表达页面描边倍率
// 可传入CopyStyle或CopyCompositeStyle,不作为几何复制快照
// 返回: GraphicObject 有效样式来源
func (m CompositeMember) Style() GraphicObject {
object := cloneEditorData(m.Object)
if object.Type == "PathObject" {
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
}
// CopyCompositeStyle 将同类型样式复制到内部选区,保留内容、位置、裁剪及其他实例
// 路径复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
// 描边复制页面实际线宽和虚线,文字沿用目标段落和基线,全部成功后提交一次撤销
// 入参: page 页面索引, path 父路径, indexes 成员序号, source Object或CompositeMember.Style取得的样式快照
// 返回: 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 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 {
member := members[i]
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 {
return err
}
object.TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
object.TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
style := copyEditorPathStyle(object.TextObject.pathStyle(), source.TextObject.pathStyle(), editorStrokeScale(source.TextObject.CTM)/member.StrokeScale)
if err := validateEditorStroke(style); err != nil {
return err
}
object.TextObject = object.TextObject.withPathStyle(style)
object.TextObject.DrawParam, err = e.neutralDrawParam()
if err != nil {
return err
+4 -2
View File
@@ -20,8 +20,9 @@ import (
"strconv"
)
// UpdateCompositeText 修改内部普通横向文字,原文保留定位,已知段落按会话选项重排
// UpdateCompositeText 修改内部文字,原文保留定位,已知段落按会话选项重排
// 空样式保留原字体、字号和颜色,显式改变字号时重新排版并保留首行基线
// 方向和字形映射沿用RewriteText的定位校验,不改变原文中未修改的属性
// 入参: page 页面索引, path 父复合路径, index 成员序号, value 文字, style 文字样式增量
// 返回: 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 文字框调整
// 返回: 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")
}
object := cloneEditorData(node.object)
+76 -22
View File
@@ -78,18 +78,31 @@ func (e *Editor) DocumentInfo(index int) (DocInfo, error) {
// 入参: index 文档索引,从0开始, info 文档信息
// 返回: 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)
if err != nil {
return err
}
if reflect.DeepEqual(before, info) {
return nil
return ctx.Err()
}
if index == e.DocumentIndex() {
if err := ctx.Err(); err != nil {
return err
}
e.SetInfo(info)
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)
if err != nil {
return 0, err
@@ -104,6 +117,16 @@ func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
// 返回: 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() {
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 {
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)
for name := range reader.fileNames {
if err := ctx.Err(); err != nil {
return 0, err
}
directory, _, _ := strings.Cut(name, "/")
occupied[directory] = true
}
directory := ""
for number := 0; ; number++ {
if err := ctx.Err(); err != nil {
return 0, err
}
candidate := fmt.Sprintf("Doc_%d", number)
if !occupied[candidate] {
directory = candidate
break
}
}
createdReader, err := created.Reader()
createdReader, err := created.ReaderContext(ctx)
if err != nil {
return 0, err
}
for name, content := range createdReader.files {
if err := ctx.Err(); err != nil {
return 0, err
}
if name == "OFD.xml" {
continue
}
@@ -168,33 +200,59 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
// 返回: 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)
if err != nil {
return err
}
info.Title = title
return e.SetDocumentInfo(index, info)
return e.SetDocumentInfoContext(ctx, index, info)
}
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
// 入参: from 原文档索引, to 目标索引,均从0开始
// 返回: 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() {
return fmt.Errorf("document index out of range")
}
if from == to {
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())
for index := range bodies {
if err := ctx.Err(); err != nil {
return 0, err
}
body := root.childAt("DocBody", index)
bodies[index] = data[body.start:body.end]
}
moveEditorItem(bodies, from, to)
var patches []editorXMLPatch
for index, content := range bodies {
if err := ctx.Err(); err != nil {
return 0, err
}
body := root.childAt("DocBody", index)
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 在独立快照中修改文档入口,确认未取消后提交完整状态
// 入参: ctx 取消上下文, edit 修改包快照并返回新的当前文档索引
// 返回: error 修改或取消错误
@@ -314,6 +361,13 @@ func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []
if err != nil {
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")
if err != nil {
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 {
reader.selectedVersion = nil
}
if _, err := reader.Doc(); err != nil {
if _, err := reader.DocContext(ctx); err != nil {
return err
}
next := *e
if err := next.adoptDocumentReader(reader); err != nil {
if err := next.adoptDocumentReader(ctx, reader); err != nil {
return err
}
if err := ctx.Err(); err != nil {
@@ -356,9 +410,9 @@ func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []
}
// adoptDocumentReader 接入已加载的编辑入口,跨文档或版本时重建页面与资源状态
// 入参: reader 不拥有输入文件的独立阅读器
// 入参: ctx 取消上下文, reader 不拥有输入文件的独立阅读器
// 返回: error 编辑器初始化错误
func (e *Editor) adoptDocumentReader(reader *Reader) error {
func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error {
version, err := reader.DocumentVersionID()
if err != nil {
return err
@@ -372,7 +426,7 @@ func (e *Editor) adoptDocumentReader(reader *Reader) error {
source.reader = reader
e.source = &source
} else {
next, err := reader.Editor()
next, err := reader.EditorContext(ctx)
if err != nil {
return err
}
+2 -2
View File
@@ -28,7 +28,7 @@ import (
// 入参: page 页面索引, ids 对象标识, 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 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 页面毫米范围
// 返回: 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 e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
+40 -46
View File
@@ -20,6 +20,7 @@ import (
"context"
"crypto/sha256"
"encoding/xml"
"errors"
"fmt"
"maps"
"slices"
@@ -530,7 +531,7 @@ func (e *Editor) restoreFontSnapshot(state Editor) {
*e = state
}
// pruneExternalFontFiles 清理全包无引用字体并同步版本清单,保留附件及无法确认的扩展引用
// pruneExternalFontFiles 流式核对全包字体引用并同步版本清单,保留附件及无法确认的扩展引用
// 入参: ctx 取消上下文, reader 包快照, candidates 待清理的字体路径
// 返回: 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
files := make(map[string]bool)
versions := make(map[string]*editorFontXML)
versions := make(map[string][]versionEditLink)
names := slices.Sorted(maps.Keys(structures))
for _, name := range slices.Sorted(maps.Keys(reader.fileNames)) {
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] {
continue
}
data, err := reader.readFileViewContext(ctx, name)
if err != nil {
return err
root, uncertain := "", false
inspect := func(node xml.StartElement) error {
if root == "" {
root = node.Name.Local
}
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return ctx.Err()
if classifyOFDNamespace(node.Name.Space) == ofdXMLUnknown || slices.Contains([]string{"CustomTags", "Extensions", "ExtendData", "Data"}, node.Name.Local) {
uncertain = true
return fmt.Errorf("unknown font file dependencies in %s", name)
}
base := ""
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 {
for _, attr := range node.Attr {
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)
if kind == packageReferenceData {
files[location] = true
}
}
}
if kind := packageReference(root.name.Local, parent, node.name.Local, ""); kind != packageReferenceNone && !(root.matchesOFD("DocVersion") && parent == "FileList" && node.matchesOFD("File")) {
location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node)))
delete(removed, cleanPackagePath(location))
if kind == packageReferenceData {
files[cleanPackagePath(location)] = true
record := func(node *editorXML, attribute, target string) {
if root == "DocVersion" && node.name.Local == "File" && attribute == "" && removed[target] {
versions[name] = append(versions[name], versionEditLink{node: node, target: target})
}
}
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()
}
}
@@ -629,23 +621,25 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
return err
}
updates := make(map[string][]byte)
for name, file := range versions {
for name, links := range versions {
if err := ctx.Err(); err != nil {
return err
}
var patches []editorXMLPatch
if list := file.root.child("FileList"); list != nil {
for _, node := range list.children {
for _, link := range links {
if err := ctx.Err(); err != nil {
return err
}
if node.matchesOFD("File") && removed[resolveResourcePath(name, "", strings.TrimSpace(editorImportText(file.data, node)))] {
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
}
if removed[link.target] {
patches = append(patches, editorXMLPatch{start: link.node.start, end: link.node.end})
}
}
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 {
+1 -1
View File
@@ -35,7 +35,7 @@ func (e *Editor) compactVersionFonts(ctx context.Context, parts map[string][]byt
if !versioned {
return nil
}
protected, err := reader.hasOutputSignatures()
protected, err := reader.hasOutputSignatures(ctx)
if err != nil || protected {
return err
}
+12 -6
View File
@@ -1212,6 +1212,15 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
var content []byte
pageContent := name == "Page" && node.parent == nil
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")
if local == nil {
var templates []Template
@@ -1234,20 +1243,17 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
if err != nil {
return nil, err
}
content = encoded
content = append(content, encoded...)
} else {
encoded, err := m.encode(entry, local)
if err != nil {
return nil, err
}
content = encoded
content = append(content, encoded...)
}
}
for _, child := range node.children {
if pageContent && entry.template && child.matchesOFD("Template") {
continue
}
if pageContent && child.name.Local == "Area" {
if pageContent && (child.matchesOFD("Area") || child.matchesOFD("Template") || child.matchesOFD("PageRes")) {
continue
}
if name == "Goto" && child.name.Local == "Bookmark" {
+20 -1
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
@@ -117,7 +118,15 @@ type PageCapabilities struct {
// 版本共享文件在提交修改时隔离,保留其他版本及其文件清单
// 返回: *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 {
return nil, err
}
@@ -144,6 +153,9 @@ func (r *Reader) Editor() (*Editor, error) {
e.pages = make([]PageContent, len(doc.Pages.Page))
sourcePages := make([]editorSourcePage, len(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 {
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.pages[i] = PageContent{ID: page.ID}
}
if err := ctx.Err(); err != nil {
return nil, err
}
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.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
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":
a, b := &before.PathObject, after.PathObject
a.ID, a.Boundary, a.CTM, a.Alpha = b.ID, b.Boundary, b.CTM, b.Alpha
a.Fill, a.Stroke, a.FillColor, a.StrokeColor, a.LineWidth = b.Fill, b.Stroke, b.FillColor, b.StrokeColor, b.LineWidth
a.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 "ImageObject":
+18 -6
View File
@@ -340,11 +340,23 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
}
patches = append(patches, editorXMLPatch{area.start, area.end, updated})
} 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 {
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 {
@@ -546,7 +558,7 @@ func (e *Editor) sourcePageData(page PageContent, references *editorReferenceSca
err := x.finish()
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 {
return nil, err
}
@@ -562,7 +574,7 @@ func (e *Editor) sourcePageData(page PageContent, references *editorReferenceSca
if err != nil {
return nil, err
}
data := append(area, content...)
data := append(bytes.TrimPrefix(header, []byte(xml.Header)), content...)
if len(page.Actions) != 0 {
actions, err := e.encodeXML(func(x *ofdXML) { x.actions(page.Actions) })
if err != nil {
@@ -754,7 +766,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
delete(reader.pageHeaderCache, name)
}
}
if _, err := reader.Doc(); err != nil {
if _, err := reader.DocContext(ctx); err != nil {
return nil, err
}
return reader, nil
@@ -768,7 +780,7 @@ func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Re
if err := reader.initRootContext(ctx); err != nil {
return nil, err
}
if _, err := reader.Doc(); err != nil {
if _, err := reader.DocContext(ctx); err != nil {
return nil, err
}
for name, header := range e.source.reader.pageHeaderCache {
+1 -1
View File
@@ -96,7 +96,7 @@ func (e *Editor) FlipSelection(page int, objects, annotations []string, axis str
// 入参: page 页面索引, objects 正文标识, annotations 注解标识, 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
}
bounds, err := e.SelectionBounds(page, objects, annotations)
+82 -40
View File
@@ -23,7 +23,7 @@ import (
)
// ObjectStyle 对象外观,nil字段保持原值,颜色的Alpha为nil时保留原颜色透明度
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的LineWidth使用页面毫米
// StyleObjects使用对象坐标毫米,StyleCompositeObjects的线宽、虚线长度及偏移使用页面毫米
type ObjectStyle struct {
Alpha *int
Fill *bool
@@ -31,6 +31,7 @@ type ObjectStyle struct {
FillColor *FillColor
StrokeColor *StrokeColor
LineWidth *float64
MiterLimit *float64
DashPattern *string
DashOffset *float64
Cap *string
@@ -251,7 +252,7 @@ func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
return e.addEditorDrawParam(cloneEditorData(draw))
}
// StyleObjects 原子更新对象透明度、文字颜色及路径填充和描边样式
// StyleObjects 原子更新对象透明度及文字、路径的填充和描边样式
// 入参: page 页面索引, ids 对象标识, style 待修改属性
// 返回: 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
}
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])
if err != nil {
return err
@@ -278,8 +279,8 @@ func (e *Editor) StyleObjects(page int, ids []string, style ObjectStyle) error {
}
// CopyStyle 将同类型对象的外观复制到选区,不复制内容、位置、资源数据或动作
// 文字复制字体、字号、颜色及透明度,沿用目标段落设置;字体标识须属于当前文档
// 路径按页面中的实际线宽和虚线长度复制,保持目标变换
// 文字复制字体、字号、填充、描边及透明度,沿用目标段落设置;字体标识须属于当前文档
// 描边按页面中的实际线宽和虚线长度复制,保持目标变换
// 入参: page 目标页面索引, ids 目标对象标识, source 样式来源快照
// 返回: 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) {
black, err := e.RGBColor(color.NRGBA{A: 255})
source, err = e.explicitDefaultPaint(source)
if err != nil {
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" {
objects, err = e.styleTextObjects(objects, TextStyle{Font: source.TextObject.Font, Size: source.TextObject.Size})
if err != nil {
return err
}
for i := range objects {
objects[i].TextObject.FillColor = cloneEditorData(source.TextObject.FillColor)
objects[i].TextObject.Alpha = cloneEditorData(source.TextObject.Alpha)
text := objects[i].TextObject
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)
}
@@ -346,6 +336,44 @@ func (e *Editor) CopyStyle(page int, ids []string, source GraphicObject) (err er
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 原子修改图片边框,保留未修改的颜色、线型及原文扩展
// 入参: page 页面索引, ids 图片标识, style 边框增量
// 返回: error 错误信息
@@ -498,26 +526,28 @@ func (e *Editor) resolveEditorStyleDefaults(object GraphicObject, base *DrawPara
if object.Type == "PathObject" {
obj := &object.PathObject
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})
obj.DrawParam = ""
obj.LineWidth, obj.Join, obj.Cap = style.LineWidth, style.Join, style.Cap
obj.LineWidthSet = style.LineWidthSet
obj.MiterLimit, obj.DashPattern, obj.DashOffset = style.MiterLimit, style.DashPattern, style.DashOffset
obj.MiterLimitSet = style.MiterLimitSet
obj.dashPatternSet = style.dashPatternSet
obj.FillColor, obj.StrokeColor = style.FillColor, style.StrokeColor
} else {
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,
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})
obj.DrawParam = ""
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.MiterLimitSet = style.MiterLimitSet
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
}
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) {
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
if (stroke || paint) && object.Type != "PathObject" {
return GraphicObject{}, fmt.Errorf("stroke style requires path objects")
if (stroke || paint) && object.Type != "PathObject" && object.Type != "TextObject" {
return GraphicObject{}, fmt.Errorf("stroke style requires text or path objects")
}
if style.FillColor != nil && object.Type != "TextObject" && object.Type != "PathObject" {
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 {
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":
if style.Alpha != nil {
object.ImageObject.Alpha = style.Alpha
@@ -566,8 +589,11 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
if style.Alpha != nil {
object.CompositeGraphicUnit.Alpha = style.Alpha
}
case "PathObject":
path := &object.PathObject
case "TextObject", "PathObject":
path := object.PathObject
if object.Type == "TextObject" {
path = object.TextObject.pathStyle()
}
if style.Alpha != nil {
path.Alpha = style.Alpha
}
@@ -603,11 +629,20 @@ func (e *Editor) styleObject(object GraphicObject, style ObjectStyle) (GraphicOb
if style.Join != nil {
path.Join = *style.Join
}
if style.MiterLimit != nil {
path.MiterLimit = *style.MiterLimit
path.MiterLimitSet = path.MiterLimit == 0
}
if stroke {
if err := validateEditorStroke(*path); err != nil {
if err := validateEditorStroke(path); err != nil {
return GraphicObject{}, err
}
}
if object.Type == "TextObject" {
object.TextObject = object.TextObject.withPathStyle(path)
} else {
object.PathObject = path
}
default:
return GraphicObject{}, fmt.Errorf("unsupported object type %q", object.Type)
}
@@ -633,7 +668,7 @@ func (e *Editor) validateCopyStyle(source GraphicObject) error {
var colors []*FillColor
switch source.Type {
case "TextObject":
alpha, colors = source.TextObject.Alpha, []*FillColor{source.TextObject.FillColor}
return e.validateCopyStyle(GraphicObject{Type: "PathObject", PathObject: source.TextObject.pathStyle()})
case "PathObject":
from := source.PathObject
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.LineWidth, to.Cap, to.Join, to.MiterLimit = from.LineWidth, from.Cap, from.Join, from.MiterLimit
to.LineWidthSet = from.LineWidthSet
to.MiterLimitSet = from.MiterLimitSet
to.DashPattern, to.DashOffset, to.dashPatternSet = from.DashPattern, from.DashOffset, true
if to.LineWidth == 0 && !to.LineWidthSet {
to.LineWidth = defaultPathLineWidth
@@ -678,7 +714,7 @@ func copyEditorPathStyle(to, from PathObject, scale float64) PathObject {
if to.Join == "" {
to.Join = "Miter"
}
if to.MiterLimit == 0 {
if to.MiterLimit == 0 && !to.MiterLimitSet {
to.MiterLimit = defaultMiterLimit
}
if to.DashOffset == nil {
@@ -757,6 +793,9 @@ func validateEditorStroke(path PathObject) error {
if err != nil {
return err
}
if len(values) < 2 {
return fmt.Errorf("dash pattern requires at least two lengths")
}
total := 0.0
for _, value := range values {
if value < 0 {
@@ -767,5 +806,8 @@ func validateEditorStroke(path PathObject) error {
if total == 0 {
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
}
+27 -4
View File
@@ -81,11 +81,22 @@ func (e *MissingGlyphError) Error() string {
// 已知段落重新排版;原布局未知时替换字体和颜色保留坐标,不允许自动调整字号
// 入参: page 页面索引, ids 文字对象标识, style 样式增量
// 返回: 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)
if err != nil {
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)
if err != nil {
return err
@@ -97,7 +108,7 @@ func (e *Editor) StyleText(page int, ids []string, style TextStyle) error {
// 原文等长替换保留全部位移,单个TextCode内增删仅调整局部步进,跨定位段增删需显式重排
// 入参: page 页面索引, id 文字对象标识, value 新内容, style 样式增量
// 返回: 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)
if err != nil {
return err
@@ -118,6 +129,17 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
}
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)
if err != nil {
return err
@@ -178,10 +200,11 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
}
codes := append([]TextCode(nil), obj.TextCode...)
transforms := append([]CGTransform(nil), obj.CGTransform...)
lines := newTextLineScanner(*obj)
if len(old) == len(next) {
offset := 0
for i := range codes {
if obj.textCodeLineBreak(i) {
if lines.next(codes[i]) {
if next[offset] != '\n' {
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
for i := range codes {
if obj.textCodeLineBreak(i) {
if lines.next(codes[i]) {
offset++
}
runes := textCodeRunes(codes[i].Value)
+29 -9
View File
@@ -82,7 +82,7 @@ func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
// 入参: page 页面索引, ids 对象标识, 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
}
objects, _, err := e.selectedObjects(page, ids)
@@ -123,10 +123,15 @@ func (obj ImageObject) ImageBounds() (Box, error) {
if obj.CTM == "" {
return box, nil
}
if _, err := creationNumbers(obj.CTM, 6); err != nil {
values, err := creationNumbers(obj.CTM, 6)
if err != nil {
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 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
@@ -519,21 +524,35 @@ func cropImageObject(image *ImageObject, box Box) error {
if err != nil {
return err
}
if _, err := creationBox(editorBoxString(box)); err != nil {
if err := validateCreationBox(box); err != nil {
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 {
return fmt.Errorf("crop must stay within the image bounds")
}
before, _ := ParseBox(image.Boundary)
matrix := NewMatrix(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 {
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)
image.Clips = nil
if !finite(y) {
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 {
path, err := NewShape(ShapeRectangle, Box{W: box.W, H: box.H})
if err != nil {
@@ -541,8 +560,9 @@ func cropImageObject(image *ImageObject, box Box) error {
}
fill, stroke := true, false
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
}
+132 -19
View File
@@ -29,14 +29,24 @@ import (
// 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 {
return nil, nil
}
reader, err := e.Reader()
reader, err := e.ReaderContext(ctx)
if err != nil {
return nil, err
}
return reader.DocumentVersions()
return reader.DocumentVersionsContext(ctx)
}
// DocumentVersionID 获取当前编辑版本的标识,主入口返回空字符串
@@ -53,22 +63,37 @@ func (e *Editor) DocumentVersionID() (string, error) {
// 入参: id 版本标识
// 返回: 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()
if err != nil || current == id {
return err
}
reader, err := e.Reader()
reader, err := e.readerContext(ctx, nil)
if err != nil {
return err
}
view, err := reader.DocumentVersion(id)
view, err := reader.DocumentVersionContext(ctx, id)
if err != nil {
return err
}
before := e.transactionSnapshot()
if err := e.adoptDocumentReader(view); err != nil {
next := *e
if err := next.adoptDocumentReader(ctx, view); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
*e = next
e.recordTransaction(before)
return nil
}
@@ -180,7 +205,7 @@ func (e *Editor) AddVersionContext(ctx context.Context, name string) (string, er
if err != nil {
return 0, err
}
data, err = editorVersionDefault(data, body, "")
data, err = editorVersionDefault(ctx, data, body, "")
if err != nil {
return 0, err
}
@@ -212,16 +237,42 @@ func (e *Editor) AddVersionContext(ctx context.Context, name string) (string, er
}
// SetDefaultVersion 设置下次打开使用的版本,不切换当前编辑入口,可通过Undo撤销
// 空标识取消默认版本并使用主入口;修改OFD.xml可能使已有签名失效
// 空标识取消默认版本并使用主入口,无主入口时不可取消;修改OFD.xml可能使已有签名失效
// 入参: id 版本标识
// 返回: 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 {
return err
}
found, changed := id == "", false
for _, version := range versions {
if err := ctx.Err(); err != nil {
return err
}
found = found || 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)
}
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()
if err != nil {
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 {
return 0, err
}
@@ -250,7 +301,24 @@ func (e *Editor) SetDefaultVersion(id string) error {
// 入参: id 版本标识, name 版本名称,空值清空名称
// 返回: 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 {
return err
}
@@ -259,15 +327,15 @@ func (e *Editor) RenameVersion(id, name string) error {
return fmt.Errorf("document version not found: %s", id)
}
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]
original, err := reader.readFile(version.Location)
original, err := reader.readFileViewContext(ctx, version.Location)
if err != nil {
return 0, err
}
descriptor, err := parseEditorXML(original)
descriptor, err := parseEditorXMLContext(ctx, original)
if err != nil {
return 0, err
}
@@ -278,6 +346,9 @@ func (e *Editor) RenameVersion(id, name string) error {
location := packageAvailableName(reader, nil, version.Location)
body := root.childAt("DocBody", reader.documentIndex)
for _, node := range body.child("Versions").children {
if err := ctx.Err(); err != nil {
return 0, err
}
if node.matchesOFD("Version") && node.attr("ID") == id {
fragment, err := editorXMLAttribute(data, node, "BaseLoc", "/"+location)
if err != nil {
@@ -288,7 +359,7 @@ func (e *Editor) RenameVersion(id, name string) error {
}
}
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 reader.documentIndex, nil
@@ -297,6 +368,7 @@ func (e *Editor) RenameVersion(id, name string) error {
// DeleteVersion 删除版本及其独占文件,可通过Undo撤销,不删除文档主入口
// 删除当前版本后转到剩余默认版本或主入口;修改入口可能使已有签名失效
// 无主入口时保留至少一个版本,需要替代默认版本时选择剩余版本中版本号最大的条目
// 入参: id 版本标识,不能为空
// 返回: 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 {
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")
for _, node := range list.children {
if err := ctx.Err(); err != nil {
@@ -334,6 +430,17 @@ func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
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 {
reader.selectedVersion = nil
}
@@ -352,15 +459,21 @@ func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
}
// editorVersionDefault 修改版本入口的Current属性,不改变条目顺序或其他属性
// 入参: data OFD入口原文, body 文档节点, id 默认版本,空值取消默认版本
// 入参: ctx 取消上下文, data OFD入口原文, body 文档节点, id 默认版本,空值取消默认版本
// 返回: []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")
if list == nil {
return data, nil
}
var patches []editorXMLPatch
for _, node := range list.children {
if err := ctx.Err(); err != nil {
return nil, err
}
if !node.matchesOFD("Version") {
continue
}
+2 -2
View File
@@ -318,7 +318,7 @@ func editorXMLSupported(node *editorXML) bool {
var allowed, children string
switch node.name.Local {
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":
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
case "Glyphs":
@@ -362,7 +362,7 @@ func editorXMLTransformable(node *editorXML) bool {
var attrs string
switch node.name.Local {
case "TextObject":
attrs = "ID Boundary CTM DrawParam Font Size HScale VScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Decoration Visible Stroke Fill Alpha"
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":
attrs = "ID Boundary CTM DrawParam LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha"
case "ImageObject":
+21 -6
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"archive/zip"
"bytes"
"context"
"encoding/xml"
"fmt"
@@ -87,7 +88,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
if r.imageCatalog != nil {
return slices.Clone(r.imageCatalog), nil
}
doc, err := r.Doc()
doc, err := r.DocContext(ctx)
if err != nil {
return nil, err
}
@@ -103,7 +104,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
header, err := r.readPageHeader(page)
header, err := r.readPageHeaderContext(ctx, page)
if err != nil {
return nil, err
}
@@ -122,15 +123,19 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
continue
}
seen[name] = true
data, err := r.readFile(name)
data, err := r.readFileViewContext(ctx, name)
if err != nil {
return nil, err
}
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
}
for _, media := range res.MultiMedias.MultiMedia {
if err := ctx.Err(); err != nil {
return nil, err
}
if media.Type != "Image" {
continue
}
@@ -150,6 +155,9 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
items = append(items, item)
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
r.imageCatalog = items
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)
if name != "" {
data, err := r.readFile(name)
data, err := r.readFileViewContext(ctx, name)
if err != nil {
return ImageInfo{}, err
}
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
}
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 != "" {
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
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
}
for _, item := range items {
if err := ctx.Err(); err != nil {
return ImageInfo{}, err
}
if imageResourceKey(item.ID) == imageResourceKey(id) {
return item, nil
}
+3 -2
View File
@@ -30,14 +30,15 @@ const (
// GeometryOperation 指定路径填充区域的布尔运算
type GeometryOperation uint8
// StrokeOptions 描述描边参数,长度单位为毫米
// StrokeOptions 描述描边参数,长度单位为毫米,MiterLimitSet区分显式零值与默认截断值
type StrokeOptions struct {
// Width为正有限描边宽度
Width float64
// Cap和Join使用OFD名称,空值分别采用Butt和Miter
Cap, Join string
// MiterLimit为尖角长度与半线宽的比值上限,0采用默认值
// MiterLimit为尖角长度与半线宽的比值上限
MiterLimit float64
MiterLimitSet bool
// DashOffset为绝对长度的虚线相位,允许负值
DashOffset float64
// Tolerance为曲线展开误差目标,0采用后端默认精度
+1 -1
View File
@@ -371,7 +371,7 @@ func (line geometryPolyline) stroke(result GeometryPath, options StrokeOptions)
return
}
limit := options.MiterLimit
if limit == 0 {
if limit == 0 && !options.MiterLimitSet {
limit = defaultMiterLimit
}
if options.Join == "" || options.Join == "Miter" {
+120
View File
@@ -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
View File
@@ -70,7 +70,7 @@ func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}
// UnmarshalXML 解析路径并区分省略样式、显式实线与零线宽
// UnmarshalXML 解析路径并区分省略样式、显式实线与零值描边参数
// 入参: d XML解码器, start 起始节点
// 返回: 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" {
p.LineWidthSet = p.LineWidth == 0
}
if attr.Name.Local == "MiterLimit" {
p.MiterLimitSet = p.MiterLimit == 0
}
}
if value.DashPattern != nil {
p.DashPattern = *value.DashPattern
@@ -95,7 +98,7 @@ func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
return nil
}
// UnmarshalXML 解析绘制参数并保留显式实线和零线宽对基础参数的覆盖
// UnmarshalXML 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖
// 入参: d XML解码器, start 起始节点
// 返回: 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" {
p.LineWidthSet = p.LineWidth == 0
}
if attr.Name.Local == "MiterLimit" {
p.MiterLimitSet = p.MiterLimit == 0
}
}
if value.DashPattern != nil {
p.DashPattern = *value.DashPattern
+8
View File
@@ -83,11 +83,15 @@ type ClipArea struct {
}
// TextObject 文本对象
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
type TextObject struct {
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
DrawParam string `xml:"DrawParam,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"`
MiterLimit float64 `xml:"MiterLimit,attr"`
Font string `xml:"Font,attr"`
@@ -111,6 +115,8 @@ type TextObject struct {
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
LineWidthSet bool `xml:"-"`
MiterLimitSet bool `xml:"-"`
dashPatternSet bool
layout *textLayout
}
@@ -171,6 +177,7 @@ type CGTransform struct {
}
// PathObject 路径对象
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
type PathObject struct {
ID string `xml:"ID,attr"`
Boundary string `xml:"Boundary,attr"`
@@ -193,6 +200,7 @@ type PathObject struct {
Clips *Clips `xml:"Clips"`
Actions []Action `xml:"Actions>Action"`
LineWidthSet bool `xml:"-"`
MiterLimitSet bool `xml:"-"`
dashPatternSet bool
}
+18 -10
View File
@@ -63,30 +63,38 @@ type MeshShading struct {
// 返回: color.RGBA 采样颜色
func (m *MeshShading) At(x, y float64) color.RGBA {
for i := len(m.Triangles) - 1; i >= 0; i-- {
t := m.Triangles[i]
a, b, c := t[0].Point, t[1].Point, t[2].Point
if value, ok := meshTriangleColor(&m.Triangles[i], x, y); ok {
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)
if d == 0 {
continue
return color.RGBA{}, false
}
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
w := 1 - u - v
if u < -1e-12 || v < -1e-12 || w < -1e-12 {
continue
return color.RGBA{}, false
}
var values [4]float64
alpha := 0.0
for j, weight := range [3]float64{u, v, w} {
alpha += weight * t[j].Alpha
alpha += weight * triangle[j].Alpha
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)
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}
}
return m.Background
red, green, blue := triangle[0].rgb(values)
return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)}, true
}
// outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用
+27 -8
View File
@@ -381,6 +381,23 @@ func pdfNumbers(values ...float64) string {
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 序列化对象边界
// 入参: b 对象边界
// 返回: 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) {
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)
}
box, err := p.pathBoundsContext(p.ctx, mark.Path)
@@ -652,7 +672,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
ctm = pdfNumbers(m[:]...)
}
paint := func(value pdfgo.Paint) (*FillColor, error) {
if value.Tiling != nil {
if value.Tiling != nil || pdfGradientPattern(value) {
return p.paintColor(value, box)
}
return drawing.paintColor(value, origin)
@@ -685,11 +705,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
object.Rule = "Even-Odd"
}
if len(mark.Style.Dash) > 0 {
values := make([]float64, len(mark.Style.Dash))
for n, v := range mark.Style.Dash {
values[n] = v * scale
}
object.DashPattern = pdfNumbers(values...)
object.DashPattern = pdfDashPattern(mark.Style.Dash, scale)
phase := mark.Style.DashPhase * scale
object.DashOffset = &phase
}
@@ -702,9 +718,12 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
// 入参: paint PDF画刷, box 对象边界
// 返回: *FillColor OFD颜色或渐变, 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
}
if pdfGradientPattern(paint) {
return p.gradientPattern(paint, box)
}
if paint.Mesh != nil {
return p.meshColor(paint, box)
}
+10 -6
View File
@@ -114,6 +114,7 @@ type pdfCompositeCache struct {
imageProcesses map[pdfImageProcessKey]*pdfgo.ImageProcess
masks map[*pdfgo.SoftMask][]pdfCompositeNode
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
linearMeshes *renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]
patterns map[pdfgo.Paint]*pdfCompositePattern
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
shadingSources map[pdfShadingSourceKey][]pdfCompositeNode
@@ -637,16 +638,19 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
}
// opaque 检查图元是否无需与页面背景混合
// 入参: space 当前混合空间
// 返回: bool 是否可直接保留, error 遮罩解析错误
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) (bool, error) {
// 入参: n 待检查图元, space 当前混合空间
// 返回: 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 {
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) {
return false, nil
}
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
}
}
@@ -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) {
return false, nil
}
if fill && pdfGradientError(style.Fill) != nil || stroke && pdfGradientError(style.Stroke) != nil {
return false, nil
if fill && p.gradientError(style.Fill) != nil || stroke && p.gradientError(style.Stroke) != nil {
return false, p.ctx.Err()
}
if n.image != nil {
masked, err := n.image.Image.HasMask()
+1 -1
View File
@@ -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()) {
directGroup = true
}
opaque, err := node.opaque(space)
opaque, err := p.opaque(node, space)
if err != nil {
return false, err
}
+112 -5
View File
@@ -18,6 +18,7 @@ import (
"fmt"
"math"
"runtime"
"slices"
"sync"
"github.com/xiaoqidun/pdfgo"
@@ -49,11 +50,58 @@ type pdfMeshSample struct {
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高洛德渐变,保留顶点和覆盖顺序
// 入参: paint PDF网格画刷, box 对象边界
// 返回: *FillColor OFD渐变, 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
}
mesh := paint.Mesh
@@ -67,11 +115,43 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
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 {
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)
if !finite(point.X) || !finite(point.Y) {
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 {
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
}
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)
}
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.Value = ""
@@ -103,6 +195,21 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
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 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
// 入参: mesh 原始曲面
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
+9 -2
View File
@@ -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) {
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
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}
object.LineWidth = lineWidth / scale
object.LineWidthSet = object.LineWidth == 0
object.Cap = []string{"Butt", "Round", "Square"}[mark.Style.Cap]
object.Join = []string{"Miter", "Round", "Bevel"}[mark.Style.Join]
object.MiterLimit = mark.Style.MiterLimit
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)
+241 -10
View File
@@ -180,10 +180,13 @@ func pdfGradientStopsError(stops []pdfgo.GradientStop, space *pdfgo.ColorSpace)
return nil
}
// pdfGradientError 检查渐变函数与源空间能否原生映射为OFD分段
// gradientError 检查渐变函数与源空间能否原生映射为OFD分段
// 入参: paint 画刷
// 返回: error 不可等价表达的渐变
func pdfGradientError(paint pdfgo.Paint) error {
// 返回: error 不可等价表达的渐变或取消错误
func (p *pdfImporter) gradientError(paint pdfgo.Paint) error {
if err := p.ctx.Err(); err != nil {
return err
}
if paint.Shading != nil {
return &pdfgo.UnsupportedError{Feature: "shading pattern state compositing"}
}
@@ -192,26 +195,254 @@ func pdfGradientError(paint pdfgo.Paint) error {
}
if paint.Mesh != nil {
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"}
}
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() {
return &pdfgo.UnsupportedError{Feature: "nonlinear calibrated mesh conversion"}
}
}
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)
}
if paint.Radial != nil {
if paint.Radial.Background != nil || paint.Radial.Bounds != nil {
return &pdfgo.UnsupportedError{Feature: "bounded radial pattern conversion"}
if paint.Radial.Background != nil && !pdfRadialNested(paint.Radial) {
return &pdfgo.UnsupportedError{Feature: "radial pattern background conversion"}
}
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 按原始函数合成渐变路径,保留填充描边的单一对象语义
+4 -1
View File
@@ -168,7 +168,10 @@ func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) erro
}
childBlocked := blocked
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()
}
if err := r.count(node.children, childBlocked); err != nil {
+147 -26
View File
@@ -139,14 +139,25 @@ func (r *Reader) DocumentPageCountContext(ctx context.Context, index int) (int,
// 入参: index 文档索引,从0开始
// 返回: *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() {
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}
if err := next.initRoot(); err != nil {
if err := next.initRootContext(ctx); err != nil {
return nil, err
}
if _, err := next.Doc(); err != nil {
if _, err := next.DocContext(ctx); err != nil {
return nil, err
}
return next, nil
@@ -155,15 +166,65 @@ func (r *Reader) Document(index int) (*Reader, error) {
// 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 {
return nil, err
}
if r.relatedPending {
r.relatedPending = false
_ = r.parseAnnotations(doc)
_ = r.parseSignatures(doc)
if view.relatedPending {
view.Annots = maps.Clone(r.Annots)
for page, entries := range view.Annots {
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
}
@@ -202,9 +263,7 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
if err := decoder.Decode(&doc); err != nil {
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
}
if doc.Signatures == "" {
doc.Signatures = view.versionSignatures(docAttr.Signatures)
}
view.resourcesRead = maps.Clone(r.resourcesRead)
view.resourceFiles = maps.Clone(r.resourceFiles)
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 {
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.resourceFiles, view.resourceFiles)
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.compositeGraphicUnitCache, view.compositeGraphicUnitCache)
r.documentRoot, r.versionInfo, r.RootDir = view.documentRoot, view.versionInfo, view.RootDir
r.doc, r.relatedPending = &doc, true
return r.doc, nil
r.doc, r.relatedPending = view.doc, view.relatedPending
}
// PageContent 获取页面内容
@@ -244,7 +310,14 @@ func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
// 入参: 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 获取当前文档总页数
@@ -261,25 +334,39 @@ func (r *Reader) PageCount() (int, error) {
// 入参: index 页面索引,从0开始
// 返回: *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 {
return nil, err
}
if index < 0 || index >= len(doc.Pages.Page) {
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 读取页面区域,不解析页面图元
// 入参: 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 {
return PageArea{}, err
}
return r.resolvePageArea(content.Area, content.Template)
return r.resolvePageAreaContext(ctx, content.Area, content.Template)
}
// ResPath 按当前文档目录解析ST_Loc,包内绝对路径以根目录为基准
@@ -842,20 +929,20 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
}
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
// 入参: page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
// 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
// 返回: *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)
data, err := r.readFile(fullPath)
data, err := r.readFileViewContext(ctx, fullPath)
if err != nil {
return nil, err
}
var content PageContent
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if textOnly {
decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))}
err = xml.NewTokenDecoder(decoder).Decode(&content)
err = xml.NewTokenDecoder(&textPageTokens{Decoder: decoder}).Decode(&content)
} else {
err = xml.Unmarshal(data, &content)
err = decoder.Decode(&content)
}
if err != nil {
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
}
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 {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return &content, nil
}
@@ -890,6 +982,16 @@ func (r *Reader) loadPageResources(page 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)
if content, ok := r.pageHeaderCache[name]; ok {
return content, nil
@@ -900,9 +1002,12 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) {
}
defer f.Close()
var content PageContent
d := xml.NewDecoder(f)
d := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx})
scan:
for {
if err := ctx.Err(); err != nil {
return PageContent{}, err
}
token, err := d.Token()
if err == io.EOF {
break
@@ -939,6 +1044,9 @@ scan:
}
}
}
if err := ctx.Err(); err != nil {
return PageContent{}, err
}
r.pageHeaderCache[name] = content
return content, nil
}
@@ -947,15 +1055,28 @@ scan:
// 入参: area 页面区域, templates 模板引用
// 返回: 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 {
return area, nil
}
for _, ref := range templates {
for _, template := range r.doc.CommonData.TemplatePage {
if err := ctx.Err(); err != nil {
return PageArea{}, err
}
if !sameResourceID(template.ID, ref.TemplateID) {
continue
}
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
content, err := r.readPageHeaderContext(ctx, Page{BaseLoc: template.BaseLoc})
if err != nil {
return PageArea{}, err
}
+8 -3
View File
@@ -235,13 +235,13 @@ func (r *Reader) documentDependencies(ctx context.Context, indexes ...int) (map[
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝无法识别的扩展节点, add 接收引用路径及类别
// 返回: 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 流式扫描文件依赖,按需记录引用节点位置,不构建完整节点树
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝未知扩展, add 接收依赖, record 接收引用位置,可为nil
// 入参: ctx 取消上下文, name 文件路径, rejectUnknown 是否拒绝未知扩展, add 接收依赖, record 接收引用位置,可为nil, inspect 检查节点,可为nil
// 返回: 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)
if err != nil {
return err
@@ -269,6 +269,11 @@ func (r *Reader) scanDocumentFileReferences(ctx context.Context, name string, re
}
switch node := token.(type) {
case xml.StartElement:
if inspect != nil {
if err := inspect(node); err != nil {
return err
}
}
parent := ""
if len(stack) == 0 {
if root != "" {
+7 -57
View File
@@ -18,7 +18,6 @@ import (
"fmt"
"math"
"strconv"
"strings"
)
// semanticCompiler 将公共页面语义编译为路径和原始图片,绘制、搜索和度量共享定位
@@ -296,8 +295,9 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
if defaults.Join != "" {
style.options.Join = defaults.Join
}
if defaults.MiterLimit > 0 {
if defaults.MiterLimit > 0 || defaults.MiterLimitSet {
style.options.MiterLimit = defaults.MiterLimit
style.options.MiterLimitSet = defaults.MiterLimitSet
}
style.options.Dashes = parseFloats(defaults.DashPattern)
if defaults.DashOffset != nil {
@@ -319,8 +319,9 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
if object.Join != "" {
style.options.Join = object.Join
}
if object.MiterLimit > 0 {
if object.MiterLimit > 0 || object.MiterLimitSet {
style.options.MiterLimit = object.MiterLimit
style.options.MiterLimitSet = object.MiterLimitSet
}
if object.DashPattern != "" || object.dashPatternSet {
style.options.Dashes = parseFloats(object.DashPattern)
@@ -337,9 +338,7 @@ func (c *semanticCompiler) objectStyle(object PathObject, defaults *DrawParam, l
style.options.Dashes[i] *= scale
}
}
if style.options.Width == 0 {
style.options.Width = 25.4 / c.renderer.DPI
}
style.options.Width = c.renderer.deviceLineWidth(style.options.Width)
return style
}
@@ -354,24 +353,11 @@ func (c *semanticCompiler) pathObject(object PathObject, state RenderState) erro
local := object
local.Boundary = "0 0 1 1"
local.CTM = ""
var path GeometryPath
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)
path, err := c.geometry.Path(local)
if err != nil {
return err
}
path, err = c.geometry.Transform(path, matrix)
}
if err != nil {
return err
}
@@ -427,7 +413,7 @@ func (c *semanticCompiler) textObject(object TextObject, state RenderState) erro
return nil
}
_, 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 {
style.fill = withFillAlpha(&FillColor{}, object.Alpha)
}
@@ -511,39 +497,3 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
}
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})
}
+3 -2
View File
@@ -140,8 +140,9 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
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)}
glyphLinear := Matrix{a: linear.a, b: linear.b, c: linear.c, d: linear.d}.Multiply(rotate)
for index, code := range object.TextCode {
if object.textCodeLineBreak(index) {
lines := newTextLineScanner(object)
for _, code := range object.TextCode {
if lines.next(code) {
textOffset++
}
runes := textCodeRunes(code.Value)
+22 -1
View File
@@ -61,6 +61,20 @@ type Renderer struct {
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 渲染器配置选项
type RendererOption func(*Renderer)
@@ -117,6 +131,13 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
// 入参: templateID 模板ID
// 返回: *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 != "" {
templateID = key
}
@@ -127,7 +148,7 @@ func (r *Renderer) loadTemplate(templateID string) *PageContent {
if !sameResourceID(tpl.ID, templateID) {
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 {
return nil
}
+24 -5
View File
@@ -40,6 +40,18 @@ type rasterStrokeGradientCanvas struct {
type rasterGradientCanvas struct {
gradient *RasterGradient
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 返回后端标识
@@ -123,7 +135,9 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
}
}
paint := canvas.Paint{Color: cmd.Paint.Color}
var sampled rasterGradientCanvas
if g := cmd.Paint.Gradient; g != nil {
sampled = newRasterGradientCanvas(g)
stops := make(canvas.Grad, len(g.Stops))
for j, stop := range g.Stops {
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:
paint = canvas.Paint{Gradient: stops.ToRadial(start, g.R0, end, g.R1)}
case RasterMesh:
paint = canvas.Paint{Gradient: rasterGradientCanvas{gradient: g}}
paint = canvas.Paint{Gradient: sampled}
default:
return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind)
}
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}
@@ -159,10 +173,10 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
if err != nil {
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.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}
if len(options.Dashes) > 0 {
style.DashOffset = options.DashOffset / options.Width
@@ -224,7 +238,12 @@ func (g rasterStrokeGradientCanvas) SetColorSpace(space canvas.ColorSpace) canva
// 入参: x 横坐标, y 纵坐标
// 返回: 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 {
return g.space.ToLinear(value)
}
+8 -2
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"image"
"math"
"slices"
"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.StrokeColor = &StrokeColor{Value: "0 0 0"}
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.textOnly = false
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
style := r.newPathStyle(dp, 0, 0, 0, nil)
style.applyPathObject(r, obj, 0, 0, 0)
if style.lineWidth == 0 {
style.lineWidth = 25.4 / r.DPI
if scale := math.Sqrt(math.Abs(m.a*m.d - m.b*m.c)); scale > 0 {
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))
}
+2 -1
View File
@@ -173,7 +173,7 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
}
}()
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)
p = p.Dash(options.DashOffset, options.Dashes...)
p = p.Flatten(tolerance)
@@ -224,6 +224,7 @@ func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, t
options.Join = "Bevel"
case canvas.MiterJoiner:
options.MiterLimit = value.Limit
options.MiterLimitSet = value.Limit == 0
}
return options
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any
case PaintLinear, PaintRadial:
return canvasShading(paint, x, y, pageH)
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:
return paint.Color
}
+26 -32
View File
@@ -35,6 +35,7 @@ type pathStyle struct {
lineWidth float64
lineCap canvas.Capper
lineJoin canvas.Joiner
lineJoinName string
miterLimit float64
dashOffset float64
dashPattern []float64
@@ -90,21 +91,35 @@ func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
}
// applyLineJoin 应用线连接样式及尖角限制
// 入参: join 线连接名称, limit 尖角限制
func (s *pathStyle) applyLineJoin(join string, limit float64) {
if limit > 0 {
// 入参: join 线连接名称, limit 尖角限制, explicit 是否显式指定零截断值
func (s *pathStyle) applyLineJoin(join string, limit float64, explicit bool) {
if limit > 0 || explicit {
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 {
case "Miter":
s.lineJoin = canvas.MiterJoin
case "Miter", "Round", "Bevel":
s.lineJoinName = join
}
switch s.lineJoinName {
case "Round":
s.lineJoin = canvas.RoundJoin
case "Bevel":
s.lineJoin = canvas.BevelJoin
}
if _, ok := s.lineJoin.(canvas.MiterJoiner); ok {
default:
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 != "" {
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 {
s.dashPattern = parseFloats(dp.DashPattern)
}
@@ -147,7 +162,7 @@ func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH f
if obj.Cap != "" {
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 {
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.scale(ctm)
if style.lineWidth == 0 {
style.lineWidth = 25.4 / r.DPI
}
style.lineWidth = r.deviceLineWidth(style.lineWidth)
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
if boundaryInCTM && parentCTM != nil {
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 {
pathCTM = localCTM
}
p := r.buildTinyFillRectPath(obj, pageH, pathCTM, bx, by)
if p == nil {
p = r.buildPath(obj, pageH, pathCTM, false)
}
p := r.buildPath(obj, pageH, pathCTM, false)
if boundaryInCTM && parentCTM != nil {
p = p.Transform(canvas.Matrix{
{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
}
// 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)
}
+10 -12
View File
@@ -108,20 +108,14 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if dp != nil {
strokeStyle.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha)
}
if obj.StrokeColor != nil {
strokeStyle.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha)
}
if obj.LineWidth > 0 || obj.LineWidthSet {
strokeStyle.lineWidth = obj.LineWidth
}
strokeStyle.applyLineJoin(obj.Join, obj.MiterLimit)
strokeObject := obj.pathStyle()
strokeObject.FillColor = nil
strokeStyle.applyPathObject(r, strokeObject, bx, by, pageH)
if strokeStyle.strokePaint == nil {
strokeStyle.strokePaint = canvas.Transparent
}
strokeStyle.scale(ctm)
if strokeStyle.lineWidth == 0 {
strokeStyle.lineWidth = 25.4 / r.DPI
}
strokeStyle.lineWidth = r.deviceLineWidth(strokeStyle.lineWidth)
transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint)
strokeClip = r.intersectCachedClipPath(strokeClip, shadingClip)
@@ -220,8 +214,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
}
codePos := 0
textPos := 0
for index, tc := range obj.TextCode {
if textRun != nil && obj.textCodeLineBreak(index) {
var lines textLineScanner
if textRun != nil {
lines = newTextLineScanner(obj)
}
for _, tc := range obj.TextCode {
if textRun != nil && lines.next(tc) {
textPos++
}
var runes []rune
+2
View File
@@ -79,6 +79,7 @@ type DrawParams struct {
}
// DrawParam 绘制参数
// MiterLimitSet为true时,零截断值覆盖继承值,否则零值表示未指定
type DrawParam struct {
ID string `xml:"ID,attr"`
Relative string `xml:"Relative,attr"`
@@ -98,6 +99,7 @@ type DrawParam struct {
FillColor *FillColor `xml:"FillColor"`
StrokeColor *StrokeColor `xml:"StrokeColor"`
LineWidthSet bool `xml:"-"`
MiterLimitSet bool `xml:"-"`
dashPatternSet bool
}
+6 -2
View File
@@ -106,14 +106,18 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
// 入参: index 页面索引,从0开始
// 返回: *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 {
return nil, err
}
if index < 0 || index >= len(doc.Pages.Page) {
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 {
return nil, err
}
+2 -2
View File
@@ -205,7 +205,7 @@ func (p PathObject) ReshapeFrame(box Box) (PathObject, error) {
if box == previous {
return p, nil
}
if _, err := creationBox(editorBoxString(box)); err != nil {
if err := validateCreationBox(box); err != nil {
return PathObject{}, err
}
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))
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
}
p.Boundary = editorBoxString(bounds)
+18 -7
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"bytes"
"context"
"encoding/asn1"
"encoding/binary"
"encoding/xml"
@@ -167,9 +168,12 @@ func parseSignatureStampBox(s string) (Box, error) {
}
// parseSignatures 解析签名文件
// 入参: doc 文档结构
// 入参: ctx 取消上下文, doc 文档结构
// 返回: 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 == "" {
return nil
}
@@ -180,11 +184,14 @@ func (r *Reader) parseSignatures(doc *Document) error {
}
defer f.Close()
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
}
pages := signaturePageNumbers(doc)
for _, sigRef := range signatures.List {
if err := ctx.Err(); err != nil {
return err
}
func(sigRef Signature) {
sigPath := resolveResourcePath(sigListPath, "", sigRef.BaseLoc)
sf, err := r.openFile(sigPath)
@@ -193,20 +200,20 @@ func (r *Reader) parseSignatures(doc *Document) error {
}
defer sf.Close()
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
}
var sealType string
var sealData []byte
if 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)
}
}
if len(sealData) == 0 && 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 {
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
} else {
@@ -217,7 +224,11 @@ func (r *Reader) parseSignatures(doc *Document) error {
if len(sealData) == 0 {
return
}
sealData = bytes.Clone(sealData)
for _, annot := range sigFile.SignedInfo.StampAnnot {
if ctx.Err() != nil {
return
}
page, ok := resourceValue(pages, annot.PageRef)
if !ok {
continue
@@ -239,7 +250,7 @@ func (r *Reader) parseSignatures(doc *Document) error {
}
}(sigRef)
}
return nil
return ctx.Err()
}
// signaturePageNumbers 获取签名页面编号索引
-2
View File
@@ -322,9 +322,7 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
if err := xml.Unmarshal(data, &doc); err != nil {
return nil, err
}
if doc.Signatures == "" {
doc.Signatures = view.versionSignatures(body.Signatures)
}
if doc.Signatures == "" {
return nil, nil
}
+25 -117
View File
@@ -34,13 +34,35 @@ type textGlyphTransform struct {
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 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
// 返回: string 原文
func (obj TextObject) Text() string {
obj.TextCode = textCodeOrigins(obj.TextCode, 0)
lines := newTextLineScanner(obj)
var text strings.Builder
for i, code := range obj.TextCode {
if obj.textCodeLineBreak(i) {
for _, code := range obj.TextCode {
if lines.next(code) {
text.WriteByte('\n')
}
if code.Index == "" {
@@ -64,120 +86,6 @@ func (tc *TextCode) GetDeltaY() []float64 {
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排版空白
// 入参: value 文本编码内容
// 返回: int 字符数量
+151
View File
@@ -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
}
+6
View File
@@ -85,6 +85,12 @@ func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement
if attr.Name.Local == "LineWidth" {
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
}
+20 -10
View File
@@ -122,7 +122,7 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
record := func(node *editorXML, attribute, target string) {
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
}
}
@@ -182,7 +182,7 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
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)
if err != nil {
return nil, err
@@ -238,9 +238,9 @@ func (r *Reader) versionChanges(ctx context.Context, changes map[string][]byte)
}
// versionFileUnchanged 分块比较原文件与待写入内容,读取失败时保留替换内容
// 入参: ctx 取消上下文, name 包内路径, data 待写入内容, buffer 非空比较缓冲区
// 入参: ctx 取消上下文, name 包内路径, data 待写入内容, buffer 复用缓冲区,比较ZIP条目时按需分配
// 返回: 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 {
return false, err
}
@@ -253,7 +253,7 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
if err := ctx.Err(); err != nil {
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]) {
return false, nil
}
@@ -270,9 +270,12 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
return false, nil
}
defer input.Close()
if len(*buffer) == 0 {
*buffer = make([]byte, 32*1024)
}
reader := imageInput{ReadCloser: input, context: ctx}
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 {
return false, ctx.Err()
}
@@ -281,11 +284,11 @@ func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, bu
}
data = data[len(block):]
}
n, err := reader.Read(buffer[:1])
n, err := reader.Read((*buffer)[:1])
return n == 0 && err == io.EOF, ctx.Err()
}
// versionOutputChanges 隔离文档或版本改动并同步删除清单,保留其他入口仍需使用的文件
// versionOutputChanges 隔离文档或版本改动并同步删除清单,失败时不修改调用方清单
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
// 返回: 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 {
parts = make(map[string][]byte)
}
buffer := make([]byte, 32*1024)
var buffer []byte
for name, data := range parts {
if generated[name].stagedLeaf(data) {
continue
}
same, err := r.versionFileUnchanged(ctx, name, data, buffer)
same, err := r.versionFileUnchanged(ctx, name, data, &buffer)
if err != nil {
return nil, err
}
@@ -339,6 +342,8 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
if err != nil {
return nil, err
}
outputRemoved := removed
removed = maps.Clone(removed)
for name := range removed {
if graph.used[name] {
delete(removed, name)
@@ -434,6 +439,11 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
if err := ctx.Err(); err != nil {
return nil, err
}
for name := range outputRemoved {
if _, ok := removed[name]; !ok {
delete(outputRemoved, name)
}
}
for source, target := range paths {
if known := generated[source]; known.stagedLeaf(parts[target]) {
generated[target] = known
+10 -2
View File
@@ -120,7 +120,7 @@ func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersi
return nil, err
}
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)
}
var patches []editorXMLPatch
@@ -133,9 +133,17 @@ func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersi
}
}
entry := body.child("DocRoot")
if entry != nil {
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
}
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")}
if info := body.child("DocInfo"); info != nil {
+10 -2
View File
@@ -104,11 +104,19 @@ func (r *Reader) DocumentVersionID() (string, error) {
// 入参: id 版本标识,空值显式选择主入口;Document可重新按Current选择默认版本
// 返回: *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}
if err := next.initRoot(); err != nil {
if err := next.initRootContext(ctx); err != nil {
return nil, err
}
if _, err := next.Doc(); err != nil {
if _, err := next.DocContext(ctx); err != nil {
return nil, err
}
return next, nil
+56 -9
View File
@@ -141,6 +141,14 @@ func (e *Editor) Reader() (*Reader, error) {
return e.reader(nil)
}
// ReaderContext 获取可取消的独立内存快照,失败时不返回部分结果
// 快照保持Reader的资源所有权规则,不改变当前编辑状态或撤销记录
// 入参: ctx 取消上下文
// 返回: *Reader 阅读器, error 准备或取消错误
func (e *Editor) ReaderContext(ctx context.Context) (*Reader, error) {
return e.readerContext(ctx, nil)
}
// Write 写入并累计字节数
// 入参: data 数据
// 返回: int 字节数, error 错误信息
@@ -276,11 +284,11 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
if err := r.initRootContext(ctx); err != nil {
return nil, err
}
if _, err := r.Doc(); err != nil {
if _, err := r.DocContext(ctx); err != nil {
return nil, err
}
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
}
@@ -411,9 +419,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
x.root("Document", nil)
x.start("CommonData", nil)
x.text("MaxUnitID", strconv.Itoa(e.maxID))
x.start("PageArea", nil)
x.text("PhysicalBox", e.pages[0].Area.PhysicalBox)
x.end("PageArea")
x.pageArea("PageArea", e.pages[0].Area)
if len(fonts)+len(images)+len(spaces) != 0 {
x.text("DocumentRes", "DocumentRes.xml")
}
@@ -620,13 +626,11 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
return fonts, images, spaces, files
}
// page 写出页面尺寸、图层和对象
// page 按标准顺序写出页面定义、图层、对象及动作
// 入参: page 页面内容
func (x *ofdXML) page(page PageContent) {
x.root("Page", nil)
x.start("Area", nil)
x.text("PhysicalBox", page.Area.PhysicalBox)
x.end("Area")
x.pageHeader(page)
x.start("Content", nil)
for _, layer := range page.Content.Layer {
if x.err != nil {
@@ -639,6 +643,34 @@ func (x *ofdXML) page(page PageContent) {
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 图层
func (x *ofdXML) layer(layer Layer) {
@@ -890,7 +922,19 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.add("LineWidth", "0")
}
attrs.number("MiterLimit", obj.MiterLimit)
if obj.MiterLimitSet && obj.MiterLimit == 0 {
attrs.add("MiterLimit", "0")
}
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 {
attrs.add("Italic", "true")
}
@@ -911,6 +955,9 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.add("LineWidth", "0")
}
attrs.number("MiterLimit", obj.MiterLimit)
if obj.MiterLimitSet && obj.MiterLimit == 0 {
attrs.add("MiterLimit", "0")
}
attrs.add("Join", obj.Join)
attrs.add("Cap", obj.Cap)
attrs.add("Rule", obj.Rule)