diff --git a/assets/webui/index.html b/assets/webui/index.html
index 539a91c..38ca3fb 100644
--- a/assets/webui/index.html
+++ b/assets/webui/index.html
@@ -573,6 +573,10 @@
修改
+
+
软件
+
+
类型
-
@@ -591,7 +595,7 @@
- 转换提示
+ 提示
diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js
index d10d623..4308303 100644
--- a/assets/webui/ofdgo.js
+++ b/assets/webui/ofdgo.js
@@ -447,6 +447,7 @@ const el = {
metaAuthor: document.querySelector("#metaAuthor"),
metaCreationDate: document.querySelector("#metaCreationDate"),
metaModDate: document.querySelector("#metaModDate"),
+ metaCreator: document.querySelector("#metaCreator"),
metaType: document.querySelector("#metaType"),
metaVersion: document.querySelector("#metaVersion"),
metaSignatures: document.querySelector("#metaSignatures"),
@@ -2877,15 +2878,19 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) {
}
function releasePageResources() {
- const pages = [...state.pageCache.values()];
+ const images = new Set(), fonts = new Set();
+ for (const page of state.pageCache.values()) {
+ for (const name of page.imageNames) images.add(name);
+ for (const font of page.fonts) fonts.add(font.name);
+ }
for (const [name, image] of state.svgImages) {
- if (!pages.some((page) => page.imageNames.includes(name))) {
+ if (!images.has(name)) {
URL.revokeObjectURL(image.url);
state.svgImages.delete(name);
}
}
for (const [name, font] of state.svgFonts) {
- if (!pages.some((page) => page.fonts.some((item) => item.name === name))) {
+ if (!fonts.has(name)) {
document.fonts.delete(font);
state.svgFonts.delete(name);
}
@@ -4063,6 +4068,7 @@ async function openDocument(options = {}) {
}
async function loadDocumentDetails(openSeq) {
+ if (openSeq !== state.openSeq || !state.doc) return;
try {
const details = await callWASM("ofdgoDocumentInfo");
if (openSeq !== state.openSeq) return;
@@ -4766,8 +4772,11 @@ async function processPageRenderQueue() {
try {
await nextFrame();
while (state.pageRenderQueue.length) {
- state.pageRenderQueue.sort(comparePageRenderTask);
- const task = state.pageRenderQueue.shift();
+ let next = 0;
+ for (let index = 1; index < state.pageRenderQueue.length; index++) {
+ if (comparePageRenderTask(state.pageRenderQueue[index], state.pageRenderQueue[next]) < 0) next = index;
+ }
+ const [task] = state.pageRenderQueue.splice(next, 1);
try {
if (task.openSeq !== state.openSeq) {
task.resolve(null);
@@ -4778,13 +4787,17 @@ async function processPageRenderQueue() {
continue;
}
const page = await callWASM("ofdgoRenderPage", task.index, state.renderBackend, state.renderDPI, displayMode() === "raster");
+ if (state.pageInFlight.get(task.key) !== task) {
+ task.resolve(null);
+ continue;
+ }
const scope = state.composite;
if (scope?.index === task.index) {
const objects = await callWASM("ofdgoCompositeObjects", task.index, scope.key);
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
}
await loadSVGFonts(page.fonts, task.openSeq);
- if (task.openSeq === state.openSeq) {
+ if (task.openSeq === state.openSeq && state.pageInFlight.get(task.key) === task) {
for (const image of page.images) {
if (!state.svgImages.has(image.name)) {
const blob = new Blob([image.bytes], { type: image.mime });
@@ -4800,7 +4813,7 @@ async function processPageRenderQueue() {
return image.decode();
}));
}
- if (task.openSeq !== state.openSeq) {
+ if (task.openSeq !== state.openSeq || state.pageInFlight.get(task.key) !== task) {
task.resolve(null);
continue;
}
@@ -4813,7 +4826,7 @@ async function processPageRenderQueue() {
} catch (err) {
task.reject(err);
} finally {
- state.pageInFlight.delete(task.key);
+ if (state.pageInFlight.get(task.key) === task) state.pageInFlight.delete(task.key);
}
}
} finally {
@@ -6397,6 +6410,7 @@ function renderMeta(keepDetails = false) {
[el.metaSubject, (doc.subject || "").trim()],
[el.metaCreationDate, formatDocumentTime(doc.creationDate)],
[el.metaModDate, formatDocumentTime(doc.modDate)],
+ [el.metaCreator, [doc.creator, doc.creatorVersion].filter(Boolean).join(" ")],
]) {
field.textContent = value;
field.parentElement.hidden = !value;
diff --git a/go.mod b/go.mod
index 131561c..0d0ed3e 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
- github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c
+ github.com/xiaoqidun/pdfgo v0.0.0-20260926024032-ab5bec77f380
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
diff --git a/go.sum b/go.sum
index eb8eec1..fb57bff 100644
--- a/go.sum
+++ b/go.sum
@@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
-github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c h1:ArBGxLqZghXkkIDtz2UqpsxXPM68qs3qVUqOvW9SkzM=
-github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c/go.mod h1:VnsFG68jDBBkMvQePZTfAyVPq1ykFK5gsExsjVSEhmM=
+github.com/xiaoqidun/pdfgo v0.0.0-20260926024032-ab5bec77f380 h1:NIKpfWSCzRi0ytSS43KFwfV0lxItrepU+QEeab1Yt1w=
+github.com/xiaoqidun/pdfgo v0.0.0-20260926024032-ab5bec77f380/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
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=
diff --git a/internal/webui/webui.go b/internal/webui/webui.go
index d90b98a..567f465 100644
--- a/internal/webui/webui.go
+++ b/internal/webui/webui.go
@@ -76,6 +76,8 @@ type DocumentInfo struct {
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
CreationDate string `json:"creationDate"`
ModDate string `json:"modDate"`
+ Creator string `json:"creator"`
+ CreatorVersion string `json:"creatorVersion,omitempty"`
PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"`
SignatureCount int `json:"signatureCount"`
@@ -467,6 +469,8 @@ func (s *Session) Summary() DocumentInfo {
}
info.CreationDate = docInfo.CreationDate
info.ModDate = docInfo.ModDate
+ info.Creator = docInfo.Creator
+ info.CreatorVersion = docInfo.CreatorVersion
}
if fonts, err := s.Reader.Fonts(); err == nil {
for _, font := range fonts {
diff --git a/ofdgo_editor.go b/ofdgo_editor.go
index 00d0b7f..8349443 100644
--- a/ofdgo_editor.go
+++ b/ofdgo_editor.go
@@ -156,6 +156,7 @@ func NewEditor() *Editor {
Info: DocInfo{
DocID: hex.EncodeToString(id[:]),
CreationDate: time.Now().Format("2006-01-02"),
+ Creator: ofdCreator,
},
fonts: make(map[string]*FontResource),
images: make(map[string]image.Point),
@@ -307,14 +308,10 @@ func (e *Editor) DeletePages(indexes []int) error {
indexes = slices.Clone(indexes)
slices.Sort(indexes)
saved := make([]PageContent, len(indexes))
- for i, index := range indexes {
- page, err := e.page(index)
- if err != nil {
- return err
- }
- saved[i] = copyEditorPage(*page)
- }
remove := func(e *Editor) {
+ for i, index := range indexes {
+ saved[i] = copyEditorPage(e.pages[index])
+ }
e.removedPages = maps.Clone(e.removedPages)
if e.removedPages == nil {
e.removedPages = make(map[string]bool)
@@ -377,11 +374,11 @@ func (e *Editor) validatePageIndexes(indexes []int) error {
// 入参: from 原页面索引, to 移动后的页面索引
// 返回: error 错误信息
func (e *Editor) MovePage(from, to int) error {
- if _, err := e.page(from); err != nil {
- return err
+ if from < 0 || from >= len(e.pages) {
+ return fmt.Errorf("page index %d out of range", from)
}
- if _, err := e.page(to); err != nil {
- return err
+ if to < 0 || to >= len(e.pages) {
+ return fmt.Errorf("page index %d out of range", to)
}
if from == to {
return nil
diff --git a/ofdgo_editor_metadata.go b/ofdgo_editor_metadata.go
index 479615a..9fe2a70 100644
--- a/ofdgo_editor_metadata.go
+++ b/ofdgo_editor_metadata.go
@@ -22,25 +22,17 @@ import (
"reflect"
)
-// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
+// sourceInfoXML 更新文档描述字段,保留未识别内容
// 返回: []byte 根索引XML, error 错误信息
func (e *Editor) sourceInfoXML() ([]byte, error) {
data, err := e.source.reader.readFile("OFD.xml")
if err != nil {
return nil, err
}
- root, err := parseEditorXML(data)
+ data, info, err := editorDocInfoXML(data, e.source.fallbackDocID)
if err != nil {
return nil, err
}
- body := root.child("DocBody")
- if body == nil {
- return nil, fmt.Errorf("document has no DocBody")
- }
- info := body.child("DocInfo")
- if info == nil {
- return nil, fmt.Errorf("document has no DocInfo")
- }
before, after := e.source.info, e.Info
var values [][2]string
for _, field := range [][3]string{
@@ -54,7 +46,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
}
data = editorXMLSetText(data, info, values)
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
- root, err = parseEditorXML(data)
+ root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
@@ -63,6 +55,44 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
return data, nil
}
+// editorCreatorXML 更新首份文档的制作软件,移除原软件版本,保留其余根索引内容
+// 入参: data 当前根索引XML, docID 缺少文档描述时使用的标识
+// 返回: []byte 修改后的XML, error 解析错误
+func editorCreatorXML(data []byte, docID string) ([]byte, error) {
+ data, info, err := editorDocInfoXML(data, docID)
+ if err != nil {
+ return nil, err
+ }
+ return editorXMLSetText(data, info, [][2]string{{"Creator", ofdCreator}, {"CreatorVersion", ""}}), nil
+}
+
+// editorDocInfoXML 获取文档描述,修改缺少描述的源文件时补建标准容器及标识
+// 入参: data 根索引XML, docID 缺少文档描述时使用的标识
+// 返回: []byte 根索引XML, *editorXML 描述节点, error 解析错误
+func editorDocInfoXML(data []byte, docID string) ([]byte, *editorXML, error) {
+ root, err := parseEditorXML(data)
+ if err != nil {
+ return nil, nil, err
+ }
+ body := root.child("DocBody")
+ if body == nil {
+ return nil, nil, fmt.Errorf("document has no DocBody")
+ }
+ if info := body.child("DocInfo"); info != nil {
+ return data, info, nil
+ }
+ info, err := editorXMLContainer("DocInfo", nil, editorXMLText("DocID", docID))
+ if err != nil {
+ return nil, nil, err
+ }
+ data = editorPatchXML(data, []editorXMLPatch{{body.open, body.open, info}})
+ root, err = parseEditorXML(data)
+ if err != nil {
+ return nil, nil, err
+ }
+ return data, root.child("DocBody").child("DocInfo"), nil
+}
+
// editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容
// 入参: data 原XML, info 文档信息节点, values 新字段
// 返回: []byte 修改后的XML, error 编码错误
diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go
index 1fd840f..50d0b4f 100644
--- a/ofdgo_editor_open.go
+++ b/ofdgo_editor_open.go
@@ -34,6 +34,7 @@ type editorSource struct {
reader *Reader
document *Document
info DocInfo
+ fallbackDocID string
directory string
pages map[string]*editorSourcePage
origins map[string]*editorObjectOrigin
@@ -142,9 +143,9 @@ func (r *Reader) Editor() (*Editor, error) {
}
e := NewEditor()
e.encryption = r.encryption
- e.Info, e.maxID = cloneEditorData(*info), maximum
directory := cleanPackagePath(reader.RootDir)
- e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), directory: directory, pages: make(map[string]*editorSourcePage, len(doc.Pages.Page)), origins: make(map[string]*editorObjectOrigin)}
+ e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), fallbackDocID: e.Info.DocID, directory: directory, pages: make(map[string]*editorSourcePage, len(doc.Pages.Page)), origins: make(map[string]*editorObjectOrigin)}
+ e.Info, e.maxID = cloneEditorData(*info), maximum
e.pages = make([]PageContent, len(doc.Pages.Page))
sourcePages := make([]editorSourcePage, len(doc.Pages.Page))
for i, page := range doc.Pages.Page {
diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go
index 1d8c9da..8a1ac0e 100644
--- a/ofdgo_editor_package.go
+++ b/ofdgo_editor_package.go
@@ -450,6 +450,24 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
return 0, err
}
reader := e.source.reader
+ if len(parts) != 0 || len(removed) != 0 {
+ name := "OFD.xml"
+ if file, ok := reader.packageFile(name); ok {
+ name = cleanPackagePath(file.Name)
+ }
+ data, ok := parts[name]
+ if !ok {
+ data, err = reader.readFile(name)
+ if err != nil {
+ return 0, err
+ }
+ }
+ data, err = editorCreatorXML(data, e.source.fallbackDocID)
+ if err != nil {
+ return 0, err
+ }
+ parts[name] = data
+ }
remaining := maps.Clone(reader.files)
if remaining == nil {
remaining = make(map[string][]byte)
diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go
index 4ee13ee..013ac13 100644
--- a/ofdgo_font_type1.go
+++ b/ofdgo_font_type1.go
@@ -23,17 +23,20 @@ import (
"strconv"
)
+// type1Program 保存解密后的Type1字形与子程序
type type1Program struct {
glyphs []type1Glyph
subrs [][]byte
lenIV int
}
+// type1Glyph 保存字形名称及指令
type type1Glyph struct {
name string
data []byte
}
+// type1Scanner 按词项和显式长度读取Type1程序
type type1Scanner struct {
data []byte
pos int
@@ -235,10 +238,12 @@ func type1HexEncoded(data []byte) bool {
return false
}
+// type1HexDigit 判断字节是否为十六进制数字
func type1HexDigit(value byte) bool {
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'
}
+// type1Space 判断PostScript空白字节
func type1Space(value byte) bool {
return value == ' ' || value == '\t' || value == '\r' || value == '\n' || value == '\f' || value == 0
}
@@ -293,10 +298,12 @@ func (s *type1Scanner) binary(length int) ([]byte, error) {
return data, nil
}
+// type1BinaryStart 判断Type1二进制数据起始操作符
func type1BinaryStart(token string) bool {
return token == "RD" || token == "-|"
}
+// type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct {
program *type1Program
args []float64
@@ -381,9 +388,10 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
if len(s.args) == 0 {
return false, fmt.Errorf("Type1 callsubr has no index")
}
- index := int(s.args[len(s.args)-1])
+ value := s.args[len(s.args)-1]
+ index := int(value)
s.args = s.args[:len(s.args)-1]
- if index < 0 || index >= len(s.program.subrs) || s.program.subrs[index] == nil {
+ if value != float64(index) || index < 0 || index >= len(s.program.subrs) || s.program.subrs[index] == nil {
return false, fmt.Errorf("invalid Type1 subroutine index %d", index)
}
ended, err := s.run(s.program.subrs[index], depth+1)
@@ -413,9 +421,12 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
// 返回: error 错误信息
func (s *type1Outline) operator(op int) error {
a := s.args
- s.args = nil
+ s.args = a[:0]
switch op {
- case 1, 3, 1200, 1201, 1202:
+ case 1200:
+ s.args = a
+ return nil
+ case 1, 3, 1201, 1202:
return nil
case 13:
if s.defined || len(a) != 2 {
@@ -501,18 +512,21 @@ func (s *type1Outline) operator(op int) error {
case 9:
s.open = false
case 1212:
- if len(a) != 2 || a[1] == 0 {
+ if len(a) < 2 || a[len(a)-1] == 0 {
return fmt.Errorf("invalid Type1 division")
}
- s.args = []float64{a[0] / a[1]}
+ a[len(a)-2] /= a[len(a)-1]
+ s.args = a[:len(a)-1]
case 1216:
if len(a) < 2 {
return fmt.Errorf("invalid Type1 callothersubr")
}
count, number := int(a[len(a)-2]), int(a[len(a)-1])
- if count < 0 || count != len(a)-2 {
+ if float64(count) != a[len(a)-2] || float64(number) != a[len(a)-1] || count < 0 || count > len(a)-2 {
return fmt.Errorf("invalid Type1 callothersubr arguments")
}
+ s.args = a[:len(a)-count-2]
+ a = a[len(a)-count-2 : len(a)-2]
switch number {
case 0:
if !s.flex || count != 3 || len(s.flexPoints) != 7 {
diff --git a/ofdgo_image.go b/ofdgo_image.go
index 27e090d..cc8d6ba 100644
--- a/ofdgo_image.go
+++ b/ofdgo_image.go
@@ -109,6 +109,9 @@ func decodeBMPImage(data []byte) (image.Image, error) {
if compression != 0 {
return nil, fmt.Errorf("unsupported bmp compression")
}
+ if bpp != 16 && bpp != 24 && bpp != 32 {
+ return nil, fmt.Errorf("unsupported bmp depth")
+ }
topDown := height < 0
if topDown {
height = -height
@@ -116,10 +119,11 @@ func decodeBMPImage(data []byte) (image.Image, error) {
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid bmp size")
}
- rowStride := ((width*int(bpp) + 31) / 32) * 4
- if offset+rowStride*height > len(data) {
+ stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
+ if stride > int64(len(data)-offset)/int64(height) {
return nil, fmt.Errorf("truncated bmp")
}
+ rowStride := int(stride)
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
srcY := y
@@ -143,8 +147,6 @@ func decodeBMPImage(data []byte) (image.Image, error) {
case 32:
p := x * 4
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
- default:
- return nil, fmt.Errorf("unsupported bmp")
}
}
}
@@ -163,12 +165,15 @@ func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
+ if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 || offset > len(data) {
+ return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
+ }
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
planes := binary.LittleEndian.Uint16(data[26:28])
bpp := binary.LittleEndian.Uint16(data[28:30])
compression := binary.LittleEndian.Uint32(data[30:34])
- if planes != 1 {
+ if planes != 1 || width <= 0 || height == 0 || int64(height) == -int64(^uint(0)>>1)-1 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
}
return offset, width, height, bpp, compression, nil
@@ -260,14 +265,22 @@ func imageWithAlpha(img image.Image, alpha *int) image.Image {
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func imagePixelSource(img image.Image) image.Image {
- if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
- if decoded, err := src.Image(); err == nil {
- return decoded
- }
+ if decoded, err := imagePixelData(img); err == nil {
+ return decoded
}
return img
}
+// imagePixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验
+// 入参: img 图片对象
+// 返回: image.Image 解码像素, error 解码错误
+func imagePixelData(img image.Image) (image.Image, error) {
+ if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
+ return src.Image()
+ }
+ return img, nil
+}
+
// imageGrayAt 获取图片灰度像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.Gray 灰度像素
diff --git a/ofdgo_meta.go b/ofdgo_meta.go
index 36f1fd7..0c3aa1a 100644
--- a/ofdgo_meta.go
+++ b/ofdgo_meta.go
@@ -16,6 +16,9 @@ package ofdgo
import "encoding/xml"
+// ofdCreator 标识本库生成的文档
+const ofdCreator = "xiaoqidun/ofdgo"
+
// OFD OFD入口结构
// 代表OFD文档的根节点
type OFD struct {
@@ -35,14 +38,16 @@ type DocBody struct {
// DocInfo 文档元数据
type DocInfo struct {
- DocID string `xml:"DocID"`
- Title string `xml:"Title"`
- Author string `xml:"Author"`
- Subject string `xml:"Subject"`
- Abstract string `xml:"Abstract"`
- CreationDate string `xml:"CreationDate"`
- ModDate string `xml:"ModDate"`
- CustomDatas *CustomDatas `xml:"CustomDatas"`
+ DocID string `xml:"DocID"`
+ Title string `xml:"Title"`
+ Author string `xml:"Author"`
+ Subject string `xml:"Subject"`
+ Abstract string `xml:"Abstract"`
+ CreationDate string `xml:"CreationDate"`
+ ModDate string `xml:"ModDate"`
+ Creator string `xml:"Creator"`
+ CreatorVersion string `xml:"CreatorVersion"`
+ CustomDatas *CustomDatas `xml:"CustomDatas"`
}
// CustomDatas 自定义数据集合
diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go
index 5dce6d1..a6f227f 100644
--- a/ofdgo_pdf.go
+++ b/ofdgo_pdf.go
@@ -69,6 +69,7 @@ type pdfImporter struct {
pageWidth float64
pageHeight float64
pendingPath *pdfgo.PathMark
+ warning func(pdfgo.Diagnostic)
}
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
@@ -92,6 +93,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
editor.SetRenderBackends(*options.Backends)
}
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
+ if !options.Strict {
+ importer.warning = func(warning pdfgo.Diagnostic) {
+ warning.Page = importer.page + 1
+ importer.report.Warnings = append(importer.report.Warnings, warning)
+ }
+ }
if options.OnProgress != nil {
if err := options.OnProgress("pages", 0, 0); err != nil {
return nil, PDFImportReport{}, err
@@ -126,16 +133,10 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.pageHeight = height
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
- visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image}
+ visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image, Warning: importer.warning}
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return importer.group(mark, walk, visitor)
}
- if !options.Strict {
- visitor.Warning = func(warning pdfgo.Diagnostic) {
- warning.Page = index + 1
- importer.report.Warnings = append(importer.report.Warnings, warning)
- }
- }
if err := reader.WalkPage(ctx, page, visitor); err != nil {
return fmt.Errorf("import PDF page %d: %w", index+1, err)
}
diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go
index 6ebb72e..980f9c7 100644
--- a/ofdgo_pdf_object.go
+++ b/ofdgo_pdf_object.go
@@ -148,7 +148,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if err := p.flushPath(); err != nil {
return err
}
- visitor := pdfgo.Visitor{Path: p.path, Image: p.image}
+ visitor := pdfgo.Visitor{Path: p.path, Image: p.image, Warning: p.warning}
visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return p.group(group, walk, visitor)
}
diff --git a/ofdgo_raster_image.go b/ofdgo_raster_image.go
index b5aaca5..140f991 100644
--- a/ofdgo_raster_image.go
+++ b/ofdgo_raster_image.go
@@ -21,7 +21,7 @@ import (
"golang.org/x/image/math/f64"
)
-// drawRasterImage 按页面变换采样图像,两后端共享相同的高质量图像插值
+// drawRasterImage 按页面变换对图像执行高质量采样
// 入参: dst 目标图像, src 源图像, m 像素坐标变换
func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, src, src.Bounds(), draw.Over, nil)
diff --git a/ofdgo_render_compile_image.go b/ofdgo_render_compile_image.go
index 231dcf9..76d4668 100644
--- a/ofdgo_render_compile_image.go
+++ b/ofdgo_render_compile_image.go
@@ -56,8 +56,17 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er
if err != nil {
return err
}
+ if object.ImageMask != "" || object.Alpha != nil && *object.Alpha < 255 {
+ img, err = imagePixelData(img)
+ if err != nil {
+ return err
+ }
+ }
if object.ImageMask != "" {
mask, err := c.renderer.ImageResource(object.ImageMask)
+ if err == nil {
+ mask, err = imagePixelData(mask)
+ }
if err != nil {
return err
}
diff --git a/ofdgo_render_output.go b/ofdgo_render_output.go
index 5654394..37b8ea6 100644
--- a/ofdgo_render_output.go
+++ b/ofdgo_render_output.go
@@ -16,7 +16,9 @@ package ofdgo
import (
"archive/zip"
+ "encoding/binary"
"fmt"
+ "hash/crc32"
"image"
"image/jpeg"
"image/png"
@@ -27,6 +29,53 @@ import (
"golang.org/x/image/draw"
)
+// creatorXMP 记录独立导出文件的制作软件
+const creatorXMP = `` + ofdCreator + ``
+
+// imageMetadataWriter 在图像固定头之后插入元数据,不缓存图像主体
+type imageMetadataWriter struct {
+ writer io.Writer
+ skip int
+ data []byte
+}
+
+// Write 交付图像数据并在头部边界写入元数据
+// 入参: data 编码后的图像片段
+// 返回: int 已处理字节数, error 写入错误
+func (w *imageMetadataWriter) Write(data []byte) (int, error) {
+ n := 0
+ if w.skip > 0 {
+ size := min(w.skip, len(data))
+ written, err := w.writer.Write(data[:size])
+ n, w.skip = written, w.skip-written
+ data = data[written:]
+ if err != nil {
+ return n, err
+ }
+ if written != size {
+ return n, io.ErrShortWrite
+ }
+ }
+ if w.skip == 0 && w.data != nil {
+ written, err := w.writer.Write(w.data)
+ if err != nil {
+ return n, err
+ }
+ if written != len(w.data) {
+ return n, io.ErrShortWrite
+ }
+ w.data = nil
+ }
+ if len(data) == 0 {
+ return n, nil
+ }
+ written, err := w.writer.Write(data)
+ if err == nil && written != len(data) {
+ err = io.ErrShortWrite
+ }
+ return n + written, err
+}
+
// RenderTo 按格式渲染单页,JPEG使用95画质
// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt
// 返回: error 错误信息
@@ -189,7 +238,13 @@ func (r *Renderer) RenderToPNG(page *PageContent, writer io.Writer) error {
if err != nil {
return err
}
- return png.Encode(writer, img)
+ text := []byte("Software\x00" + ofdCreator)
+ chunk := make([]byte, len(text)+12)
+ binary.BigEndian.PutUint32(chunk, uint32(len(text)))
+ copy(chunk[4:], "tEXt")
+ copy(chunk[8:], text)
+ binary.BigEndian.PutUint32(chunk[len(chunk)-4:], crc32.ChecksumIEEE(chunk[4:len(chunk)-4]))
+ return png.Encode(&imageMetadataWriter{writer: writer, skip: 33, data: chunk}, img)
}
// RenderToJPEG 渲染为白底JPEG,像素尺寸由DPI决定
@@ -200,7 +255,12 @@ func (r *Renderer) RenderToJPEG(page *PageContent, writer io.Writer, options *jp
if err != nil {
return err
}
- return jpeg.Encode(writer, fillWhiteBackground(img), options)
+ metadata := []byte("http://ns.adobe.com/xap/1.0/\x00" + creatorXMP)
+ segment := make([]byte, len(metadata)+4)
+ segment[0], segment[1] = 0xff, 0xe1
+ binary.BigEndian.PutUint16(segment[2:], uint16(len(metadata)+2))
+ copy(segment[4:], metadata)
+ return jpeg.Encode(&imageMetadataWriter{writer: writer, skip: 2, data: segment}, fillWhiteBackground(img), options)
}
// fillWhiteBackground 填充白色背景,复用当前导出图片的RGBA像素
diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go
index 58a36a5..8a139cd 100644
--- a/ofdgo_renderer_canvas.go
+++ b/ofdgo_renderer_canvas.go
@@ -64,7 +64,10 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20}
for i, cmd := range page.Commands {
if cmd.Image != nil {
- source := cmd.Image
+ source, err := imagePixelData(cmd.Image)
+ if err != nil {
+ return nil, fmt.Errorf("canvas image %d: %w", i, err)
+ }
if _, linear := space.(canvas.LinearColorSpace); !linear {
converted := image.NewRGBA(source.Bounds())
for y := source.Bounds().Min.Y; y < source.Bounds().Max.Y; y++ {
diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go
index 95a2fea..dd25023 100644
--- a/ofdgo_renderer_image_canvas.go
+++ b/ofdgo_renderer_image_canvas.go
@@ -35,23 +35,38 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
r.measureImage(ctx, obj, pageH, parentCTM, boundaryInCTM, parentClip)
return
}
- if obj.Visible != nil && !*obj.Visible {
+ if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
return
}
resPath, ok := r.Reader.ResMap[obj.ResourceID]
if !ok {
+ r.renderError = fmt.Errorf("image resource %q not found", obj.ResourceID)
return
}
img, err := r.decodeImageResource(resPath)
if err != nil {
+ r.renderError = err
return
}
box, _ := ParseBox(obj.Boundary)
- if maskPath, ok := r.Reader.ResMap[obj.ImageMask]; ok {
- if mask, err := r.decodeImageResource(maskPath); err == nil {
- img = imageWithMask(img, mask)
+ if obj.ImageMask != "" || obj.Alpha != nil && *obj.Alpha < 255 {
+ img, err = imagePixelData(img)
+ if err != nil {
+ r.renderError = err
+ return
}
}
+ if obj.ImageMask != "" {
+ mask, err := r.ImageResource(obj.ImageMask)
+ if err == nil {
+ mask, err = imagePixelData(mask)
+ }
+ if err != nil {
+ r.renderError = err
+ return
+ }
+ img = imageWithMask(img, mask)
+ }
img = imageWithAlpha(img, obj.Alpha)
imgBounds := img.Bounds()
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
@@ -82,6 +97,9 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
}
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, localCTM, parentCTM, boundaryInCTM))
img = r.imageWithClip(img, clipPath, m, 96)
+ if r.renderError != nil {
+ return
+ }
m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy()))
img, pad := imageWithTransparentEdge(img)
if pad > 0 {
@@ -158,6 +176,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
} else if clipPath.Contains(imagePath) {
return img
}
+ source, err := imagePixelData(img)
+ if err != nil {
+ r.renderError = err
+ return img
+ }
+ img = source
scale := math.Max(math.Hypot(m[0][0], m[1][0]), math.Hypot(m[0][1], m[1][1])) * dpi / 25.4
scale = math.Min(scale, math.Sqrt((16<<20)/(float64(w)*float64(h))))
if scale > 1 {
@@ -195,7 +219,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
if mask.Opaque() {
return img
}
- source := imagePixelSource(img)
+ source = img
out := &image.NRGBA{Pix: mask.Pix, Stride: mask.Stride, Rect: bounds}
if src, ok := source.(*image.NRGBA); ok {
for y := 0; y < h; y++ {
diff --git a/ofdgo_renderer_output_canvas.go b/ofdgo_renderer_output_canvas.go
index 3d6aaae..c143ff2 100644
--- a/ofdgo_renderer_output_canvas.go
+++ b/ofdgo_renderer_output_canvas.go
@@ -44,6 +44,7 @@ func (CanvasBackend) RenderSVG(r *Renderer, page *PageContent, writer io.Writer,
}
buffer := bufio.NewWriter(writer)
renderer := svg.New(buffer, c.W, c.H, nil)
+ io.WriteString(buffer, svgCreatorMetadata)
c.RenderTo(renderer)
if err := renderer.Close(); err != nil {
return SVGResources{}, err
@@ -65,7 +66,7 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
options := ps.DefaultOptions
options.Format = ps.EncapsulatedPostScript
buffer := bufio.NewWriter(writer)
- renderer := ps.New(buffer, c.W, c.H, &options)
+ renderer := ps.New(canvasEPSWriter{buffer}, c.W, c.H, &options)
c.RenderTo(renderer)
if err := renderer.Close(); err != nil {
return err
@@ -73,6 +74,24 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
return buffer.Flush()
}
+// canvasEPSWriter 在EPS头部写入本库制作软件,不改动绘图内容
+type canvasEPSWriter struct {
+ io.Writer
+}
+
+// Write 写入EPS片段并替换独立的制作软件声明
+// 入参: data EPS片段
+// 返回: int 已处理字节数, error 写入错误
+func (w canvasEPSWriter) Write(data []byte) (int, error) {
+ if bytes.Equal(data, []byte("%%Creator: tdewolff/canvas\n")) {
+ if _, err := io.WriteString(w.Writer, "%%Creator: "+ofdCreator+"\n"); err != nil {
+ return 0, err
+ }
+ return len(data), nil
+ }
+ return w.Writer.Write(data)
+}
+
// RenderPDF 使用canvas输出PDF,保留导航与文字并复用输出缓冲
// 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度
// 返回: error 错误信息
@@ -95,7 +114,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
if docInfo, err := r.Reader.DocInfo(); err == nil {
info = *docInfo
}
- p.SetInfo(info.Title, info.Subject, "", info.Author, "xiaoqidun/ofdgo")
+ p.SetInfo(info.Title, info.Subject, "", info.Author, ofdCreator)
for i, page := range pages {
if i > 0 {
p.NewPage(page.Box.W, page.Box.H)
@@ -134,7 +153,7 @@ func replacePDFProducer(data []byte) []byte {
if idx < 0 {
return data
}
- dst := []byte("/Producer(xiaoqidun/ofdgo)")
+ dst := []byte("/Producer(" + ofdCreator + ")")
copy(data[idx:idx+len(old)], dst)
return data
}
diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go
index fcc20fc..f4e8688 100644
--- a/ofdgo_renderer_svg_canvas.go
+++ b/ofdgo_renderer_svg_canvas.go
@@ -30,6 +30,9 @@ import (
"github.com/tdewolff/canvas/renderers/svg"
)
+// svgCreatorMetadata 记录SVG制作软件,不参与页面绘制
+const svgCreatorMetadata = `` + creatorXMP + ``
+
// svgResourceRenderer 分离外部资源的SVG渲染器
type svgResourceRenderer struct {
*svg.SVG
@@ -69,6 +72,7 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
writer: buffer,
seen: make(map[string]bool),
}
+ io.WriteString(buffer, svgCreatorMetadata)
if images {
s.imageNames = make(map[image.Image]string)
}
diff --git a/ofdgo_sign_timestamp.go b/ofdgo_sign_timestamp.go
index bf723ab..7560023 100644
--- a/ofdgo_sign_timestamp.go
+++ b/ofdgo_sign_timestamp.go
@@ -76,6 +76,7 @@ type SignatureTimestampReport struct {
Error string
}
+// signatureTimestampEvidence 保存时间戳令牌及其绑定数据
type signatureTimestampEvidence struct {
Token []byte
Data []byte
diff --git a/ofdgo_writer.go b/ofdgo_writer.go
index 17b6cdf..48b2323 100644
--- a/ofdgo_writer.go
+++ b/ofdgo_writer.go
@@ -48,6 +48,7 @@ func (progress editorProgress) report(stage string, completed, total int) error
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
+// 制作软件统一为xiaoqidun/ofdgo,未修改的文档保留原制作软件及版本
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用
// 入参: writer 输出流
// 返回: int64 已写入字节数, error 错误信息
@@ -251,7 +252,7 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
{"DocID", e.Info.DocID}, {"Title", e.Info.Title}, {"Author", e.Info.Author},
{"Subject", e.Info.Subject}, {"Abstract", e.Info.Abstract},
{"CreationDate", e.Info.CreationDate}, {"ModDate", e.Info.ModDate},
- {"Creator", "xiaoqidun/ofdgo"},
+ {"Creator", ofdCreator},
} {
if field[1] != "" {
x.text(field[0], field[1])