mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
215d405c76
commit
71e67be9c1
14 files changed
+596
-36
No files matched your search
@@ -0,0 +1,281 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活。
|
||||
type editorResourceRefs struct {
|
||||
ids map[string]bool
|
||||
files map[string]bool
|
||||
}
|
||||
|
||||
// pruneSourceResources 在保存副本中清理无引用字体和图片,保留其他资源及原始编辑状态。
|
||||
// 按GB/T 33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档。
|
||||
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过。
|
||||
// 入参: parts 已修改和新增的包内条目
|
||||
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
||||
func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool, error) {
|
||||
if len(parts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
reader := e.source.reader
|
||||
names := make(map[string]bool)
|
||||
for name := range reader.fileIndex {
|
||||
names[name] = true
|
||||
}
|
||||
for name := range reader.files {
|
||||
names[name] = true
|
||||
}
|
||||
for name := range parts {
|
||||
names[name] = true
|
||||
}
|
||||
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
||||
var resources []string
|
||||
for _, name := range slices.Sorted(maps.Keys(names)) {
|
||||
if strings.HasSuffix(name, "/") {
|
||||
continue
|
||||
}
|
||||
var input io.ReadCloser
|
||||
if data, ok := parts[name]; ok {
|
||||
input = io.NopCloser(bytes.NewReader(data))
|
||||
} else {
|
||||
var err error
|
||||
input, err = reader.openFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resource, safe := refs.scan(input, name)
|
||||
input.Close()
|
||||
if !safe {
|
||||
return nil, nil
|
||||
}
|
||||
if resource {
|
||||
resources = append(resources, name)
|
||||
}
|
||||
}
|
||||
removed := make(map[string]bool)
|
||||
usedFiles := maps.Clone(refs.files)
|
||||
updates := make(map[string][]byte)
|
||||
for _, name := range resources {
|
||||
data, ok := parts[name]
|
||||
if !ok {
|
||||
var err error
|
||||
data, err = reader.readFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
for _, group := range root.children {
|
||||
var members []editorXMLPatch
|
||||
for _, node := range group.children {
|
||||
field := ""
|
||||
if group.name.Local == "Fonts" && node.name.Local == "Font" {
|
||||
field = "FontFile"
|
||||
} else if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" {
|
||||
field = "MediaFile"
|
||||
}
|
||||
if field == "" {
|
||||
continue
|
||||
}
|
||||
id := editorResourceID(node.attr("ID"))
|
||||
keep := id == "" || refs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
|
||||
if file := node.child(field); file != nil {
|
||||
var value string
|
||||
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
||||
if keep {
|
||||
usedFiles[location] = true
|
||||
usedFiles[strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)))] = true
|
||||
} else {
|
||||
removed[location] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !keep {
|
||||
members = append(members, editorXMLPatch{start: node.start, end: node.end})
|
||||
}
|
||||
}
|
||||
if len(members) > 0 && len(members) == len(group.children) {
|
||||
patches = append(patches, editorXMLPatch{start: group.start, end: group.end})
|
||||
} else {
|
||||
patches = append(patches, members...)
|
||||
}
|
||||
}
|
||||
if len(patches) != 0 {
|
||||
updates[name] = editorPatchXML(data, patches)
|
||||
}
|
||||
}
|
||||
for name := range usedFiles {
|
||||
delete(removed, name)
|
||||
}
|
||||
maps.Copy(parts, updates)
|
||||
for name := range parts {
|
||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
||||
delete(parts, name)
|
||||
}
|
||||
}
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
// scan 流式读取包内XML引用,二进制条目只探测文件头,不展开页面对象或图片像素。
|
||||
// 入参: input 文件流, name 包内路径
|
||||
// 返回: bool 是否资源索引, bool 是否可完整判断引用
|
||||
func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
||||
buffer := bufio.NewReader(input)
|
||||
header, err := buffer.Peek(512)
|
||||
if err != nil && err != io.EOF {
|
||||
return false, false
|
||||
}
|
||||
header = bytes.TrimSpace(bytes.TrimPrefix(header, []byte{0xef, 0xbb, 0xbf}))
|
||||
if len(header) == 0 {
|
||||
return false, err == io.EOF && !strings.EqualFold(path.Ext(name), ".xml")
|
||||
}
|
||||
if header[0] != '<' {
|
||||
return false, !strings.EqualFold(path.Ext(name), ".xml")
|
||||
}
|
||||
decoder := xml.NewDecoder(buffer)
|
||||
var stack []string
|
||||
var text strings.Builder
|
||||
resource, base := false, ""
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
return resource, len(stack) == 0
|
||||
}
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
switch token := token.(type) {
|
||||
case xml.StartElement:
|
||||
if token.Name.Space != "" && token.Name.Space != ofdNamespace {
|
||||
return false, false
|
||||
}
|
||||
if token.Name.Local == "CustomTags" || token.Name.Local == "Extensions" || token.Name.Local == "ExtendData" || token.Name.Local == "Data" {
|
||||
return false, false
|
||||
}
|
||||
if len(stack) == 0 {
|
||||
resource = token.Name.Local == "Res"
|
||||
if resource {
|
||||
for _, attr := range token.Attr {
|
||||
if attr.Name.Local == "BaseLoc" {
|
||||
base = attr.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, attr := range token.Attr {
|
||||
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
|
||||
continue
|
||||
}
|
||||
if attr.Name.Space != "" {
|
||||
return false, false
|
||||
}
|
||||
if attr.Name.Local != "ID" {
|
||||
r.reference(name, base, attr.Name.Local, attr.Value, resource)
|
||||
}
|
||||
}
|
||||
stack = append(stack, token.Name.Local)
|
||||
text.Reset()
|
||||
case xml.CharData:
|
||||
text.Write(token)
|
||||
case xml.EndElement:
|
||||
r.reference(name, base, token.Name.Local, text.String(), resource)
|
||||
stack = stack[:len(stack)-1]
|
||||
text.Reset()
|
||||
case xml.Directive:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reference 记录标准资源标识和路径引用,不将普通数字或资源自身ID视为引用。
|
||||
// 入参: name 当前文件, base 资源目录, key 字段, value 值, resource 是否资源索引
|
||||
func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
switch key {
|
||||
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "DefaultCS", "Thumbnail", "TemplateID", "PageID", "PageRef", "RefId":
|
||||
if id := editorResourceID(value); id != "" {
|
||||
r.ids[id] = true
|
||||
}
|
||||
return
|
||||
case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "FileRef", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File":
|
||||
case "BaseLoc":
|
||||
if resource {
|
||||
return
|
||||
}
|
||||
case "FontFile", "MediaFile":
|
||||
if resource {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
r.files[editorResourceLocation(name, base, value)] = true
|
||||
r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true
|
||||
}
|
||||
|
||||
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示。
|
||||
// 入参: value 标识文本
|
||||
// 返回: string 标准十进制标识,无效时为空
|
||||
func editorResourceID(value string) string {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32)
|
||||
if err != nil || id == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatUint(id, 10)
|
||||
}
|
||||
|
||||
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件。
|
||||
// 入参: name 所在XML, base 资源目录, value 路径值
|
||||
// 返回: string 包内路径
|
||||
func editorResourceLocation(name, base, value string) string {
|
||||
value = strings.ReplaceAll(strings.TrimSpace(value), "\\", "/")
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(value, "/") {
|
||||
return strings.ToLower(cleanPackagePath(value))
|
||||
}
|
||||
base = strings.ReplaceAll(base, "\\", "/")
|
||||
dir := path.Dir(name)
|
||||
if strings.HasPrefix(base, "/") {
|
||||
dir = cleanPackagePath(base)
|
||||
} else {
|
||||
dir = path.Join(dir, base)
|
||||
}
|
||||
return strings.ToLower(cleanPackagePath(path.Join(dir, value)))
|
||||
}
|
||||
Reference in new issue
Block a user