mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:16:55 +08:00
322 lines
9.2 KiB
Go
322 lines
9.2 KiB
Go
// 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// editorFontDocument 保存一个文档的字体声明与完整内容引用,不进入附件和签名封装
|
|
type editorFontDocument struct {
|
|
reader *Reader
|
|
index int
|
|
files map[string]*editorFontXML
|
|
fonts map[string]*editorFontDeclaration
|
|
unsafe bool
|
|
}
|
|
|
|
// editorFontXML 保存内容XML及其直接文件引用
|
|
type editorFontXML struct {
|
|
name string
|
|
data []byte
|
|
root *editorXML
|
|
links []editorFontLink
|
|
texts []*editorXML
|
|
}
|
|
|
|
// editorFontLink 记录需要随共享资源隔离而更新的路径
|
|
type editorFontLink struct {
|
|
node *editorXML
|
|
attribute string
|
|
target string
|
|
}
|
|
|
|
// editorFontDeclaration 关联字体声明、所属资源文件及嵌入数据路径
|
|
type editorFontDeclaration struct {
|
|
font Font
|
|
file *editorFontXML
|
|
node *editorXML
|
|
location string
|
|
}
|
|
|
|
// scanEditorFonts 按标准内容路径扫描正文、模板、注解、复合图元、图案及文字裁剪
|
|
// 入参: ctx 取消上下文, reader 包快照, index 文档索引
|
|
// 返回: *editorFontDocument 字体引用, error 读取或结构错误
|
|
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) {
|
|
scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration)}
|
|
entry, err := reader.documentEntry(index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
queue := []string{entry}
|
|
for len(queue) != 0 {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
name := queue[0]
|
|
queue = queue[1:]
|
|
if scan.files[name] != nil {
|
|
continue
|
|
}
|
|
data, err := reader.readFile(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file := &editorFontXML{name: name, data: data, root: root}
|
|
scan.files[name] = file
|
|
base := ""
|
|
if root.matchesOFD("Res") {
|
|
base = root.attr("BaseLoc")
|
|
}
|
|
var visit func(*editorXML) error
|
|
visit = func(node *editorXML) error {
|
|
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
|
|
scan.unsafe = true
|
|
return nil
|
|
}
|
|
if node.matchesOFD("Attachments") || node.matchesOFD("Signatures") {
|
|
return nil
|
|
}
|
|
if slices.Contains([]string{"CustomTags", "Extensions", "ExtendData", "Data"}, node.name.Local) {
|
|
scan.unsafe = true
|
|
return nil
|
|
}
|
|
for _, attr := range node.attrs {
|
|
if attr.Name.Space != "" && attr.Name.Space != "xmlns" && attr.Name.Space != "http://www.w3.org/XML/1998/namespace" {
|
|
scan.unsafe = true
|
|
}
|
|
}
|
|
if node.matchesOFD("Font") && node.parent != nil && node.parent.matchesOFD("Fonts") && root.matchesOFD("Res") {
|
|
if node.childAt("FontFile", 1) != nil {
|
|
return fmt.Errorf("multiple font files for %s", node.attr("ID"))
|
|
}
|
|
var font Font
|
|
if err := xml.Unmarshal(data[node.start:node.end], &font); err != nil {
|
|
return err
|
|
}
|
|
id := editorResourceID(font.ID)
|
|
if id == "" || scan.fonts[id] != nil {
|
|
return fmt.Errorf("missing or duplicate font ID %q", font.ID)
|
|
}
|
|
location := ""
|
|
if font.FontFile != "" {
|
|
location = resolveResourcePath(name, base, font.FontFile)
|
|
}
|
|
scan.fonts[id] = &editorFontDeclaration{font: font, file: file, node: node, location: location}
|
|
}
|
|
if node.matchesOFD("TextObject") || node.matchesOFD("Text") && node.parent != nil && node.parent.matchesOFD("Area") {
|
|
file.texts = append(file.texts, node)
|
|
if node.attr("Font") == "" {
|
|
scan.unsafe = true
|
|
}
|
|
}
|
|
attribute, value := "", ""
|
|
switch node.name.Local {
|
|
case "PublicRes", "DocumentRes", "PageRes", "Annotations":
|
|
value = strings.TrimSpace(editorImportText(data, node))
|
|
case "Page", "TemplatePage", "CompositeGraphicUnit":
|
|
attribute, value = "BaseLoc", node.attr("BaseLoc")
|
|
case "PageAnnot":
|
|
attribute, value = "FileLoc", node.attr("FileLoc")
|
|
case "DrawParam":
|
|
attribute, value = "Link", node.attr("Link")
|
|
case "FileLoc":
|
|
if node.parent != nil && node.parent.matchesOFD("Page") && root.matchesOFD("Annotations") {
|
|
value = strings.TrimSpace(editorImportText(data, node))
|
|
}
|
|
}
|
|
if value != "" {
|
|
target := resolveResourcePath(name, base, value)
|
|
file.links = append(file.links, editorFontLink{node: node, attribute: attribute, target: target})
|
|
queue = append(queue, target)
|
|
}
|
|
for _, child := range node.children {
|
|
if err := visit(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := visit(root); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return scan, nil
|
|
}
|
|
|
|
// apply 隔离其他文档共享的内容XML,再提交字体修改及上层路径引用
|
|
// 入参: ctx 取消上下文, changes 按原文件路径保存的修改
|
|
// 返回: error 引用扫描或XML修改错误
|
|
func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byte) error {
|
|
if s.reader.OFD.DocBody[s.index].versioned {
|
|
view := *s.reader
|
|
if s.index != view.documentIndex {
|
|
view.selectedVersion = nil
|
|
}
|
|
view.documentIndex = s.index
|
|
view.documentRoot, view.versionInfo = "", nil
|
|
parts, err := view.versionChanges(ctx, changes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
next := *s.reader
|
|
next.files = maps.Clone(next.files)
|
|
if next.files == nil {
|
|
next.files = make(map[string][]byte)
|
|
}
|
|
maps.Copy(next.files, parts)
|
|
if err := next.initRoot(); err != nil {
|
|
return err
|
|
}
|
|
*s.reader = next
|
|
return nil
|
|
}
|
|
shared := make(map[string]bool)
|
|
if s.reader.DocumentCount() > 1 {
|
|
for index := range s.reader.DocumentCount() {
|
|
if index == s.index {
|
|
continue
|
|
}
|
|
files, err := s.reader.documentFiles(ctx, index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
maps.Copy(shared, files)
|
|
}
|
|
}
|
|
paths := make(map[string]string)
|
|
for name := range changes {
|
|
if shared[name] {
|
|
paths[name] = packageAvailableName(s.reader, changes, name+".font.xml")
|
|
}
|
|
}
|
|
for changed := true; changed; {
|
|
changed = false
|
|
for name, file := range s.files {
|
|
if paths[name] != "" || !shared[name] {
|
|
continue
|
|
}
|
|
for _, link := range file.links {
|
|
if paths[link.target] != "" {
|
|
paths[name] = packageAvailableName(s.reader, changes, name+".font.xml")
|
|
changed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, name := range slices.Sorted(maps.Keys(s.files)) {
|
|
file := s.files[name]
|
|
if !slices.ContainsFunc(file.links, func(link editorFontLink) bool { return paths[link.target] != "" }) {
|
|
continue
|
|
}
|
|
data, modified := changes[name]
|
|
if !modified {
|
|
data = file.data
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var patches []editorXMLPatch
|
|
var patch func(*editorXML) error
|
|
patch = func(node *editorXML) error {
|
|
for _, link := range file.links {
|
|
target := paths[link.target]
|
|
if target == "" || node.name != link.node.name {
|
|
continue
|
|
}
|
|
if link.attribute != "" && node.attr(link.attribute) == link.node.attr(link.attribute) {
|
|
fragment, err := editorXMLAttribute(data, node, link.attribute, "/"+target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := parseEditorXML(fragment)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if node.open == node.end {
|
|
patches = append(patches, editorXMLPatch{node.start, node.end, fragment})
|
|
} else {
|
|
patches = append(patches, editorXMLPatch{node.start, node.open, fragment[:header.open]})
|
|
}
|
|
break
|
|
}
|
|
if link.attribute == "" && editorImportText(data, node) == editorImportText(file.data, link.node) {
|
|
patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+target)))
|
|
break
|
|
}
|
|
}
|
|
for _, child := range node.children {
|
|
if err := patch(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := patch(root); err != nil {
|
|
return err
|
|
}
|
|
if len(patches) != 0 {
|
|
data, modified = editorPatchXML(data, patches), true
|
|
}
|
|
if modified {
|
|
changes[name] = data
|
|
}
|
|
}
|
|
rootName, err := s.reader.documentEntry(s.index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if target := paths[rootName]; target != "" {
|
|
data, err := s.reader.readFile("OFD.xml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node := root.childAt("DocBody", s.index).child("DocRoot")
|
|
changes["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))})
|
|
}
|
|
for name, data := range changes {
|
|
if target := paths[name]; target != "" {
|
|
name = target
|
|
}
|
|
s.reader.files[name] = data
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// editorFontXMLText 编码资源路径文本,不改变包内名称
|
|
// 入参: value 原始文本
|
|
// 返回: []byte XML文本
|
|
func editorFontXMLText(value string) []byte {
|
|
var data bytes.Buffer
|
|
_ = xml.EscapeText(&data, []byte(value))
|
|
return data.Bytes()
|
|
}
|