mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
8d91752943
commit
31e67b2721
5 files changed
+221
-11
No files matched your search
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927125911-04f5476a9051
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927142759-12ed20917f60
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927125911-04f5476a9051 h1:VhoxfD1yFJMBcgThFZCo480VVBEU5dB8qAZXWCUSeqI=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927142759-12ed20917f60 h1:0JC6RK3DMb11RIVoaSSlprHo9oAZb+YyfUThEsTs0Bk=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927125911-04f5476a9051/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927142759-12ed20917f60/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+141
-2
@@ -20,14 +20,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Attachments 获取当前编辑状态的附件列表,返回值与编辑器相互独立
|
||||||
|
// 返回: []Attachment 附件信息, error 结构错误
|
||||||
|
func (e *Editor) Attachments() ([]Attachment, error) {
|
||||||
|
if e.source == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return e.source.reader.Attachments()
|
||||||
|
}
|
||||||
|
|
||||||
// AddAttachment 添加文档附件,保留原始数据,一次操作计入一条撤销记录
|
// AddAttachment 添加文档附件,保留原始数据,一次操作计入一条撤销记录
|
||||||
// 入参: name 显示文件名, data 文件数据
|
// 入参: name 显示文件名, data 文件数据
|
||||||
// 返回: string 附件标识, error 文件名或文档结构错误
|
// 返回: string 附件标识, error 文件名或文档结构错误
|
||||||
func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
||||||
if strings.TrimSpace(name) == "" {
|
if err := validateAttachmentName(name); err != nil {
|
||||||
return "", fmt.Errorf("attachment name is required")
|
return "", err
|
||||||
}
|
}
|
||||||
if err := e.prepareSourceIDs(); err != nil {
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -103,3 +114,131 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenameAttachment 修改附件显示名称,保留格式、内容及扩展字段,相同名称不产生撤销记录
|
||||||
|
// 入参: id 附件标识, name 显示名称
|
||||||
|
// 返回: error 名称或附件结构错误
|
||||||
|
func (e *Editor) RenameAttachment(id, name string) error {
|
||||||
|
if err := validateAttachmentName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.editAttachment(id, func(data []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
||||||
|
if node.attr("Name") == name {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return editorXMLAttribute(data, node, "Name", name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceAttachment 替换附件内容,使用独立资源避免影响共享引用,保留名称和格式
|
||||||
|
// 入参: id 附件标识, data 文件数据,调用后可复用
|
||||||
|
// 返回: error 附件结构或提交错误
|
||||||
|
func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||||
|
return e.editAttachment(id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
||||||
|
if err := e.prepareSourceIDs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
file := e.packageName("Attachments/Attachment_" + e.nextID())
|
||||||
|
var location bytes.Buffer
|
||||||
|
if err := xml.EscapeText(&location, []byte("/"+file)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if child := node.child("FileLoc"); child != nil {
|
||||||
|
fragment = editorPatchXML(fragment, []editorXMLPatch{editorXMLContent(fragment, child, location.Bytes())})
|
||||||
|
} else if node.open == node.end {
|
||||||
|
fragment = editorPatchXML(fragment, []editorXMLPatch{editorXMLContent(fragment, node, editorXMLText("FileLoc", "/"+file))})
|
||||||
|
} else {
|
||||||
|
fragment = editorPatchXML(fragment, []editorXMLPatch{{node.open, node.open, editorXMLText("FileLoc", "/"+file)}})
|
||||||
|
}
|
||||||
|
for _, attr := range []struct{ name, value string }{
|
||||||
|
{"Size", ofdNumber(float64(len(data)) / 1024)}, {"ModDate", time.Now().UTC().Format(time.RFC3339)},
|
||||||
|
} {
|
||||||
|
root, err := parseEditorXML(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fragment, err = editorXMLAttribute(fragment, root, attr.name, attr.value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[file] = bytes.Clone(data)
|
||||||
|
return fragment, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// editAttachment 对单个附件片段执行原子编辑,兼容独立附件文件及内联声明
|
||||||
|
// 入参: id 附件标识, change 片段及资源修改函数
|
||||||
|
// 返回: error 结构、标识或提交错误
|
||||||
|
func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[string][]byte) ([]byte, error)) error {
|
||||||
|
base := e.source
|
||||||
|
if base == nil {
|
||||||
|
return fmt.Errorf("attachment not found: %s", id)
|
||||||
|
}
|
||||||
|
name := cleanPackagePath(base.reader.OFD.DocBody[0].DocRoot)
|
||||||
|
external := strings.TrimSpace(base.document.Attachments.Path) != ""
|
||||||
|
if external {
|
||||||
|
name = base.reader.ResPath(base.document.Attachments.Path)
|
||||||
|
}
|
||||||
|
data, err := base.reader.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !external {
|
||||||
|
root = root.child("Attachments")
|
||||||
|
}
|
||||||
|
var found *editorXML
|
||||||
|
if root != nil {
|
||||||
|
for _, child := range root.children {
|
||||||
|
if child.name.Local == "Attachment" && child.attr("ID") == id {
|
||||||
|
if found != nil {
|
||||||
|
return fmt.Errorf("duplicate attachment ID: %s", id)
|
||||||
|
}
|
||||||
|
found = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
return fmt.Errorf("attachment not found: %s", id)
|
||||||
|
}
|
||||||
|
fragment, err := editorXMLStandalone(data[found.start:found.end], found)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
node, err := parseEditorXML(fragment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts := make(map[string][]byte)
|
||||||
|
updated, err := change(fragment, node, parts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(updated, fragment) && len(parts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts[name] = editorPatchXML(data, []editorXMLPatch{{found.start, found.end, updated}})
|
||||||
|
return e.commitAnnotationParts(base, parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAttachmentName 校验非空附件名及XML可无损表示的Unicode字符
|
||||||
|
// 入参: name 显示名称
|
||||||
|
// 返回: error 名称错误
|
||||||
|
func validateAttachmentName(name string) error {
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
return fmt.Errorf("attachment name is required")
|
||||||
|
}
|
||||||
|
if !utf8.ValidString(name) {
|
||||||
|
return fmt.Errorf("attachment name is not valid UTF-8")
|
||||||
|
}
|
||||||
|
for _, char := range name {
|
||||||
|
if char < 0x20 && char != '\t' && char != '\n' && char != '\r' || char == 0xfffe || char == 0xffff {
|
||||||
|
return fmt.Errorf("attachment name contains an invalid XML character")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+10
-2
@@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -600,7 +601,7 @@ func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
|||||||
return doc.Bookmarks.Bookmark, nil
|
return doc.Bookmarks.Bookmark, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attachments 获取附件列表
|
// Attachments 获取独立的附件列表,修改返回值不影响阅读器缓存
|
||||||
// 返回: []Attachment 附件列表, error 错误信息
|
// 返回: []Attachment 附件列表, error 错误信息
|
||||||
func (r *Reader) Attachments() ([]Attachment, error) {
|
func (r *Reader) Attachments() ([]Attachment, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.Doc()
|
||||||
@@ -624,7 +625,14 @@ func (r *Reader) Attachments() ([]Attachment, error) {
|
|||||||
}
|
}
|
||||||
doc.Attachments.Attachment = attachments.Attachment
|
doc.Attachments.Attachment = attachments.Attachment
|
||||||
}
|
}
|
||||||
return doc.Attachments.Attachment, nil
|
attachments := slices.Clone(doc.Attachments.Attachment)
|
||||||
|
for i := range attachments {
|
||||||
|
if attachments[i].Size != nil {
|
||||||
|
size := *attachments[i].Size
|
||||||
|
attachments[i].Size = &size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据
|
// OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据
|
||||||
|
|||||||
+67
-4
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -171,16 +172,42 @@ func (p *PageText) writeTo(writer io.Writer, separator bool) (int64, error) {
|
|||||||
// 返回: []TextMatch 匹配结果及上下文
|
// 返回: []TextMatch 匹配结果及上下文
|
||||||
func (p *PageText) Search(query string) []TextMatch {
|
func (p *PageText) Search(query string) []TextMatch {
|
||||||
var matches []TextMatch
|
var matches []TextMatch
|
||||||
|
_ = p.SearchEach(context.Background(), query, func(match TextMatch) error {
|
||||||
|
matches = append(matches, match)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchEach 逐项返回忽略大小写的字面匹配,不累积全部结果,在对象与匹配之间检查取消
|
||||||
|
// 入参: ctx 取消上下文, query 搜索文字, visit 匹配访问函数,可返回错误停止
|
||||||
|
// 返回: error 取消或访问错误
|
||||||
|
func (p *PageText) SearchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.searchEach(ctx, foldText(query), visit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchEach 使用预折叠的查询执行搜索,供跨页搜索复用
|
||||||
|
// 入参: ctx 取消上下文, query 已折叠查询, visit 匹配访问函数
|
||||||
|
// 返回: error 取消或访问错误
|
||||||
|
func (p *PageText) searchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
|
||||||
if query == "" {
|
if query == "" {
|
||||||
return matches
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
query = foldText(query)
|
|
||||||
length := utf8.RuneCountInString(query)
|
length := utf8.RuneCountInString(query)
|
||||||
for index, run := range p.Runs {
|
for index, run := range p.Runs {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
text := foldText(run.Text)
|
text := foldText(run.Text)
|
||||||
var runes []rune
|
var runes []rune
|
||||||
codeOffset := 0
|
codeOffset := 0
|
||||||
for offset := 0; offset < len(text); {
|
for offset := 0; offset < len(text); {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
found := strings.Index(text[offset:], query)
|
found := strings.Index(text[offset:], query)
|
||||||
if found < 0 {
|
if found < 0 {
|
||||||
break
|
break
|
||||||
@@ -203,12 +230,48 @@ func (p *PageText) Search(query string) []TextMatch {
|
|||||||
match.Boxes = append(match.Boxes, box)
|
match.Boxes = append(match.Boxes, box)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matches = append(matches, match)
|
if err := visit(match); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
offset += len(query)
|
offset += len(query)
|
||||||
codeOffset = end
|
codeOffset = end
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matches
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchText 逐页搜索原文并立即返回匹配,不保留整本文字或识别图像
|
||||||
|
// 入参: ctx 取消上下文,页面提取之间及匹配时检查, query 搜索文字, visit 零基页码和匹配访问函数, indices 页码,省略则全部,按原页序去重
|
||||||
|
// 返回: error 页面提取、取消或访问错误
|
||||||
|
func (r *Renderer) SearchText(ctx context.Context, query string, visit func(int, TextMatch) error, indices ...int) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if query == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
count, err := r.Reader.PageCount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indices, err = exportPageIndices(count, indices)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
query = foldText(query)
|
||||||
|
for _, index := range indices {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
text, err := r.PageTextByIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to search page %d: %w", index+1, err)
|
||||||
|
}
|
||||||
|
if err := text.searchEach(ctx, query, func(match TextMatch) error { return visit(index, match) }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// foldText 统一Unicode大小写等价字符,保留字符数量
|
// foldText 统一Unicode大小写等价字符,保留字符数量
|
||||||
|
|||||||
Reference in new issue
Block a user