mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
35631cdf98
commit
c3908819bb
23 files changed
+303
-81
No files matched your search
@@ -573,6 +573,10 @@
|
|||||||
<dt>修改</dt>
|
<dt>修改</dt>
|
||||||
<dd id="metaModDate"></dd>
|
<dd id="metaModDate"></dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div hidden>
|
||||||
|
<dt>软件</dt>
|
||||||
|
<dd id="metaCreator"></dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>类型</dt>
|
<dt>类型</dt>
|
||||||
<dd id="metaType">-</dd>
|
<dd id="metaType">-</dd>
|
||||||
@@ -591,7 +595,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<details id="conversionNotice" class="conversion-notice" hidden>
|
<details id="conversionNotice" class="conversion-notice" hidden>
|
||||||
<summary>转换提示</summary>
|
<summary>提示</summary>
|
||||||
<div id="conversionWarnings" class="attachment-detail"></div>
|
<div id="conversionWarnings" class="attachment-detail"></div>
|
||||||
</details>
|
</details>
|
||||||
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
|
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
|
||||||
|
|||||||
+22
-8
@@ -447,6 +447,7 @@ const el = {
|
|||||||
metaAuthor: document.querySelector("#metaAuthor"),
|
metaAuthor: document.querySelector("#metaAuthor"),
|
||||||
metaCreationDate: document.querySelector("#metaCreationDate"),
|
metaCreationDate: document.querySelector("#metaCreationDate"),
|
||||||
metaModDate: document.querySelector("#metaModDate"),
|
metaModDate: document.querySelector("#metaModDate"),
|
||||||
|
metaCreator: document.querySelector("#metaCreator"),
|
||||||
metaType: document.querySelector("#metaType"),
|
metaType: document.querySelector("#metaType"),
|
||||||
metaVersion: document.querySelector("#metaVersion"),
|
metaVersion: document.querySelector("#metaVersion"),
|
||||||
metaSignatures: document.querySelector("#metaSignatures"),
|
metaSignatures: document.querySelector("#metaSignatures"),
|
||||||
@@ -2877,15 +2878,19 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function releasePageResources() {
|
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) {
|
for (const [name, image] of state.svgImages) {
|
||||||
if (!pages.some((page) => page.imageNames.includes(name))) {
|
if (!images.has(name)) {
|
||||||
URL.revokeObjectURL(image.url);
|
URL.revokeObjectURL(image.url);
|
||||||
state.svgImages.delete(name);
|
state.svgImages.delete(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [name, font] of state.svgFonts) {
|
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);
|
document.fonts.delete(font);
|
||||||
state.svgFonts.delete(name);
|
state.svgFonts.delete(name);
|
||||||
}
|
}
|
||||||
@@ -4063,6 +4068,7 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDocumentDetails(openSeq) {
|
async function loadDocumentDetails(openSeq) {
|
||||||
|
if (openSeq !== state.openSeq || !state.doc) return;
|
||||||
try {
|
try {
|
||||||
const details = await callWASM("ofdgoDocumentInfo");
|
const details = await callWASM("ofdgoDocumentInfo");
|
||||||
if (openSeq !== state.openSeq) return;
|
if (openSeq !== state.openSeq) return;
|
||||||
@@ -4766,8 +4772,11 @@ async function processPageRenderQueue() {
|
|||||||
try {
|
try {
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
while (state.pageRenderQueue.length) {
|
while (state.pageRenderQueue.length) {
|
||||||
state.pageRenderQueue.sort(comparePageRenderTask);
|
let next = 0;
|
||||||
const task = state.pageRenderQueue.shift();
|
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 {
|
try {
|
||||||
if (task.openSeq !== state.openSeq) {
|
if (task.openSeq !== state.openSeq) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
@@ -4778,13 +4787,17 @@ async function processPageRenderQueue() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const page = await callWASM("ofdgoRenderPage", task.index, state.renderBackend, state.renderDPI, displayMode() === "raster");
|
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;
|
const scope = state.composite;
|
||||||
if (scope?.index === task.index) {
|
if (scope?.index === task.index) {
|
||||||
const objects = await callWASM("ofdgoCompositeObjects", task.index, scope.key);
|
const objects = await callWASM("ofdgoCompositeObjects", task.index, scope.key);
|
||||||
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
if (scope === state.composite && task.openSeq === state.openSeq) scope.objects = objects;
|
||||||
}
|
}
|
||||||
await loadSVGFonts(page.fonts, task.openSeq);
|
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) {
|
for (const image of page.images) {
|
||||||
if (!state.svgImages.has(image.name)) {
|
if (!state.svgImages.has(image.name)) {
|
||||||
const blob = new Blob([image.bytes], { type: image.mime });
|
const blob = new Blob([image.bytes], { type: image.mime });
|
||||||
@@ -4800,7 +4813,7 @@ async function processPageRenderQueue() {
|
|||||||
return image.decode();
|
return image.decode();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (task.openSeq !== state.openSeq) {
|
if (task.openSeq !== state.openSeq || state.pageInFlight.get(task.key) !== task) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4813,7 +4826,7 @@ async function processPageRenderQueue() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
task.reject(err);
|
task.reject(err);
|
||||||
} finally {
|
} finally {
|
||||||
state.pageInFlight.delete(task.key);
|
if (state.pageInFlight.get(task.key) === task) state.pageInFlight.delete(task.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -6397,6 +6410,7 @@ function renderMeta(keepDetails = false) {
|
|||||||
[el.metaSubject, (doc.subject || "").trim()],
|
[el.metaSubject, (doc.subject || "").trim()],
|
||||||
[el.metaCreationDate, formatDocumentTime(doc.creationDate)],
|
[el.metaCreationDate, formatDocumentTime(doc.creationDate)],
|
||||||
[el.metaModDate, formatDocumentTime(doc.modDate)],
|
[el.metaModDate, formatDocumentTime(doc.modDate)],
|
||||||
|
[el.metaCreator, [doc.creator, doc.creatorVersion].filter(Boolean).join(" ")],
|
||||||
]) {
|
]) {
|
||||||
field.textContent = value;
|
field.textContent = value;
|
||||||
field.parentElement.hidden = !value;
|
field.parentElement.hidden = !value;
|
||||||
|
|||||||
@@ -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-20260926005801-0f4b07fb311c
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926024032-ab5bec77f380
|
||||||
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-20260926005801-0f4b07fb311c h1:ArBGxLqZghXkkIDtz2UqpsxXPM68qs3qVUqOvW9SkzM=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260926024032-ab5bec77f380 h1:NIKpfWSCzRi0ytSS43KFwfV0lxItrepU+QEeab1Yt1w=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c/go.mod h1:VnsFG68jDBBkMvQePZTfAyVPq1ykFK5gsExsjVSEhmM=
|
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 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=
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ type DocumentInfo struct {
|
|||||||
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
|
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
|
||||||
CreationDate string `json:"creationDate"`
|
CreationDate string `json:"creationDate"`
|
||||||
ModDate string `json:"modDate"`
|
ModDate string `json:"modDate"`
|
||||||
|
Creator string `json:"creator"`
|
||||||
|
CreatorVersion string `json:"creatorVersion,omitempty"`
|
||||||
PageCount int `json:"pageCount"`
|
PageCount int `json:"pageCount"`
|
||||||
FontCount int `json:"fontCount"`
|
FontCount int `json:"fontCount"`
|
||||||
SignatureCount int `json:"signatureCount"`
|
SignatureCount int `json:"signatureCount"`
|
||||||
@@ -467,6 +469,8 @@ func (s *Session) Summary() DocumentInfo {
|
|||||||
}
|
}
|
||||||
info.CreationDate = docInfo.CreationDate
|
info.CreationDate = docInfo.CreationDate
|
||||||
info.ModDate = docInfo.ModDate
|
info.ModDate = docInfo.ModDate
|
||||||
|
info.Creator = docInfo.Creator
|
||||||
|
info.CreatorVersion = docInfo.CreatorVersion
|
||||||
}
|
}
|
||||||
if fonts, err := s.Reader.Fonts(); err == nil {
|
if fonts, err := s.Reader.Fonts(); err == nil {
|
||||||
for _, font := range fonts {
|
for _, font := range fonts {
|
||||||
|
|||||||
+8
-11
@@ -156,6 +156,7 @@ func NewEditor() *Editor {
|
|||||||
Info: DocInfo{
|
Info: DocInfo{
|
||||||
DocID: hex.EncodeToString(id[:]),
|
DocID: hex.EncodeToString(id[:]),
|
||||||
CreationDate: time.Now().Format("2006-01-02"),
|
CreationDate: time.Now().Format("2006-01-02"),
|
||||||
|
Creator: ofdCreator,
|
||||||
},
|
},
|
||||||
fonts: make(map[string]*FontResource),
|
fonts: make(map[string]*FontResource),
|
||||||
images: make(map[string]image.Point),
|
images: make(map[string]image.Point),
|
||||||
@@ -307,14 +308,10 @@ func (e *Editor) DeletePages(indexes []int) error {
|
|||||||
indexes = slices.Clone(indexes)
|
indexes = slices.Clone(indexes)
|
||||||
slices.Sort(indexes)
|
slices.Sort(indexes)
|
||||||
saved := make([]PageContent, len(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) {
|
remove := func(e *Editor) {
|
||||||
|
for i, index := range indexes {
|
||||||
|
saved[i] = copyEditorPage(e.pages[index])
|
||||||
|
}
|
||||||
e.removedPages = maps.Clone(e.removedPages)
|
e.removedPages = maps.Clone(e.removedPages)
|
||||||
if e.removedPages == nil {
|
if e.removedPages == nil {
|
||||||
e.removedPages = make(map[string]bool)
|
e.removedPages = make(map[string]bool)
|
||||||
@@ -377,11 +374,11 @@ func (e *Editor) validatePageIndexes(indexes []int) error {
|
|||||||
// 入参: from 原页面索引, to 移动后的页面索引
|
// 入参: from 原页面索引, to 移动后的页面索引
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) MovePage(from, to int) error {
|
func (e *Editor) MovePage(from, to int) error {
|
||||||
if _, err := e.page(from); err != nil {
|
if from < 0 || from >= len(e.pages) {
|
||||||
return err
|
return fmt.Errorf("page index %d out of range", from)
|
||||||
}
|
}
|
||||||
if _, err := e.page(to); err != nil {
|
if to < 0 || to >= len(e.pages) {
|
||||||
return err
|
return fmt.Errorf("page index %d out of range", to)
|
||||||
}
|
}
|
||||||
if from == to {
|
if from == to {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+41
-11
@@ -22,25 +22,17 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
|
// sourceInfoXML 更新文档描述字段,保留未识别内容
|
||||||
// 返回: []byte 根索引XML, error 错误信息
|
// 返回: []byte 根索引XML, error 错误信息
|
||||||
func (e *Editor) sourceInfoXML() ([]byte, error) {
|
func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||||
data, err := e.source.reader.readFile("OFD.xml")
|
data, err := e.source.reader.readFile("OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
data, info, err := editorDocInfoXML(data, e.source.fallbackDocID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
before, after := e.source.info, e.Info
|
||||||
var values [][2]string
|
var values [][2]string
|
||||||
for _, field := range [][3]string{
|
for _, field := range [][3]string{
|
||||||
@@ -54,7 +46,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
data = editorXMLSetText(data, info, values)
|
data = editorXMLSetText(data, info, values)
|
||||||
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
|
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
|
||||||
root, err = parseEditorXML(data)
|
root, err := parseEditorXML(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -63,6 +55,44 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
|||||||
return data, nil
|
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 更新自定义字段,保留原字段及容器的扩展内容
|
// editorCustomDatas 更新自定义字段,保留原字段及容器的扩展内容
|
||||||
// 入参: data 原XML, info 文档信息节点, values 新字段
|
// 入参: data 原XML, info 文档信息节点, values 新字段
|
||||||
// 返回: []byte 修改后的XML, error 编码错误
|
// 返回: []byte 修改后的XML, error 编码错误
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type editorSource struct {
|
|||||||
reader *Reader
|
reader *Reader
|
||||||
document *Document
|
document *Document
|
||||||
info DocInfo
|
info DocInfo
|
||||||
|
fallbackDocID string
|
||||||
directory string
|
directory string
|
||||||
pages map[string]*editorSourcePage
|
pages map[string]*editorSourcePage
|
||||||
origins map[string]*editorObjectOrigin
|
origins map[string]*editorObjectOrigin
|
||||||
@@ -142,9 +143,9 @@ func (r *Reader) Editor() (*Editor, error) {
|
|||||||
}
|
}
|
||||||
e := NewEditor()
|
e := NewEditor()
|
||||||
e.encryption = r.encryption
|
e.encryption = r.encryption
|
||||||
e.Info, e.maxID = cloneEditorData(*info), maximum
|
|
||||||
directory := cleanPackagePath(reader.RootDir)
|
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))
|
e.pages = make([]PageContent, len(doc.Pages.Page))
|
||||||
sourcePages := make([]editorSourcePage, len(doc.Pages.Page))
|
sourcePages := make([]editorSourcePage, len(doc.Pages.Page))
|
||||||
for i, page := range doc.Pages.Page {
|
for i, page := range doc.Pages.Page {
|
||||||
|
|||||||
@@ -450,6 +450,24 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
reader := e.source.reader
|
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)
|
remaining := maps.Clone(reader.files)
|
||||||
if remaining == nil {
|
if remaining == nil {
|
||||||
remaining = make(map[string][]byte)
|
remaining = make(map[string][]byte)
|
||||||
|
|||||||
+21
-7
@@ -23,17 +23,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// type1Program 保存解密后的Type1字形与子程序
|
||||||
type type1Program struct {
|
type type1Program struct {
|
||||||
glyphs []type1Glyph
|
glyphs []type1Glyph
|
||||||
subrs [][]byte
|
subrs [][]byte
|
||||||
lenIV int
|
lenIV int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1Glyph 保存字形名称及指令
|
||||||
type type1Glyph struct {
|
type type1Glyph struct {
|
||||||
name string
|
name string
|
||||||
data []byte
|
data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1Scanner 按词项和显式长度读取Type1程序
|
||||||
type type1Scanner struct {
|
type type1Scanner struct {
|
||||||
data []byte
|
data []byte
|
||||||
pos int
|
pos int
|
||||||
@@ -235,10 +238,12 @@ func type1HexEncoded(data []byte) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1HexDigit 判断字节是否为十六进制数字
|
||||||
func type1HexDigit(value byte) bool {
|
func type1HexDigit(value byte) bool {
|
||||||
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'
|
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1Space 判断PostScript空白字节
|
||||||
func type1Space(value byte) bool {
|
func type1Space(value byte) bool {
|
||||||
return value == ' ' || value == '\t' || value == '\r' || value == '\n' || value == '\f' || value == 0
|
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
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1BinaryStart 判断Type1二进制数据起始操作符
|
||||||
func type1BinaryStart(token string) bool {
|
func type1BinaryStart(token string) bool {
|
||||||
return token == "RD" || token == "-|"
|
return token == "RD" || token == "-|"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type1Outline 保存字形指令的操作数栈及轮廓位置
|
||||||
type type1Outline struct {
|
type type1Outline struct {
|
||||||
program *type1Program
|
program *type1Program
|
||||||
args []float64
|
args []float64
|
||||||
@@ -381,9 +388,10 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
|
|||||||
if len(s.args) == 0 {
|
if len(s.args) == 0 {
|
||||||
return false, fmt.Errorf("Type1 callsubr has no index")
|
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]
|
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)
|
return false, fmt.Errorf("invalid Type1 subroutine index %d", index)
|
||||||
}
|
}
|
||||||
ended, err := s.run(s.program.subrs[index], depth+1)
|
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 错误信息
|
// 返回: error 错误信息
|
||||||
func (s *type1Outline) operator(op int) error {
|
func (s *type1Outline) operator(op int) error {
|
||||||
a := s.args
|
a := s.args
|
||||||
s.args = nil
|
s.args = a[:0]
|
||||||
switch op {
|
switch op {
|
||||||
case 1, 3, 1200, 1201, 1202:
|
case 1200:
|
||||||
|
s.args = a
|
||||||
|
return nil
|
||||||
|
case 1, 3, 1201, 1202:
|
||||||
return nil
|
return nil
|
||||||
case 13:
|
case 13:
|
||||||
if s.defined || len(a) != 2 {
|
if s.defined || len(a) != 2 {
|
||||||
@@ -501,18 +512,21 @@ func (s *type1Outline) operator(op int) error {
|
|||||||
case 9:
|
case 9:
|
||||||
s.open = false
|
s.open = false
|
||||||
case 1212:
|
case 1212:
|
||||||
if len(a) != 2 || a[1] == 0 {
|
if len(a) < 2 || a[len(a)-1] == 0 {
|
||||||
return fmt.Errorf("invalid Type1 division")
|
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:
|
case 1216:
|
||||||
if len(a) < 2 {
|
if len(a) < 2 {
|
||||||
return fmt.Errorf("invalid Type1 callothersubr")
|
return fmt.Errorf("invalid Type1 callothersubr")
|
||||||
}
|
}
|
||||||
count, number := int(a[len(a)-2]), int(a[len(a)-1])
|
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")
|
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 {
|
switch number {
|
||||||
case 0:
|
case 0:
|
||||||
if !s.flex || count != 3 || len(s.flexPoints) != 7 {
|
if !s.flex || count != 3 || len(s.flexPoints) != 7 {
|
||||||
|
|||||||
+22
-9
@@ -109,6 +109,9 @@ func decodeBMPImage(data []byte) (image.Image, error) {
|
|||||||
if compression != 0 {
|
if compression != 0 {
|
||||||
return nil, fmt.Errorf("unsupported bmp compression")
|
return nil, fmt.Errorf("unsupported bmp compression")
|
||||||
}
|
}
|
||||||
|
if bpp != 16 && bpp != 24 && bpp != 32 {
|
||||||
|
return nil, fmt.Errorf("unsupported bmp depth")
|
||||||
|
}
|
||||||
topDown := height < 0
|
topDown := height < 0
|
||||||
if topDown {
|
if topDown {
|
||||||
height = -height
|
height = -height
|
||||||
@@ -116,10 +119,11 @@ func decodeBMPImage(data []byte) (image.Image, error) {
|
|||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return nil, fmt.Errorf("invalid bmp size")
|
return nil, fmt.Errorf("invalid bmp size")
|
||||||
}
|
}
|
||||||
rowStride := ((width*int(bpp) + 31) / 32) * 4
|
stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
|
||||||
if offset+rowStride*height > len(data) {
|
if stride > int64(len(data)-offset)/int64(height) {
|
||||||
return nil, fmt.Errorf("truncated bmp")
|
return nil, fmt.Errorf("truncated bmp")
|
||||||
}
|
}
|
||||||
|
rowStride := int(stride)
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
srcY := y
|
srcY := y
|
||||||
@@ -143,8 +147,6 @@ func decodeBMPImage(data []byte) (image.Image, error) {
|
|||||||
case 32:
|
case 32:
|
||||||
p := x * 4
|
p := x * 4
|
||||||
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
|
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 {
|
if dibSize < 40 {
|
||||||
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
|
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])))
|
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
|
||||||
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
|
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
|
||||||
planes := binary.LittleEndian.Uint16(data[26:28])
|
planes := binary.LittleEndian.Uint16(data[26:28])
|
||||||
bpp := binary.LittleEndian.Uint16(data[28:30])
|
bpp := binary.LittleEndian.Uint16(data[28:30])
|
||||||
compression := binary.LittleEndian.Uint32(data[30:34])
|
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 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
|
||||||
}
|
}
|
||||||
return offset, width, height, bpp, compression, nil
|
return offset, width, height, bpp, compression, nil
|
||||||
@@ -260,14 +265,22 @@ func imageWithAlpha(img image.Image, alpha *int) image.Image {
|
|||||||
// 入参: img 图片对象
|
// 入参: img 图片对象
|
||||||
// 返回: image.Image 图片像素源
|
// 返回: image.Image 图片像素源
|
||||||
func imagePixelSource(img image.Image) image.Image {
|
func imagePixelSource(img image.Image) image.Image {
|
||||||
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
|
if decoded, err := imagePixelData(img); err == nil {
|
||||||
if decoded, err := src.Image(); err == nil {
|
return decoded
|
||||||
return decoded
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return img
|
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 获取图片灰度像素
|
// imageGrayAt 获取图片灰度像素
|
||||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||||
// 返回: color.Gray 灰度像素
|
// 返回: color.Gray 灰度像素
|
||||||
|
|||||||
+13
-8
@@ -16,6 +16,9 @@ package ofdgo
|
|||||||
|
|
||||||
import "encoding/xml"
|
import "encoding/xml"
|
||||||
|
|
||||||
|
// ofdCreator 标识本库生成的文档
|
||||||
|
const ofdCreator = "xiaoqidun/ofdgo"
|
||||||
|
|
||||||
// OFD OFD入口结构
|
// OFD OFD入口结构
|
||||||
// 代表OFD文档的根节点
|
// 代表OFD文档的根节点
|
||||||
type OFD struct {
|
type OFD struct {
|
||||||
@@ -35,14 +38,16 @@ type DocBody struct {
|
|||||||
|
|
||||||
// DocInfo 文档元数据
|
// DocInfo 文档元数据
|
||||||
type DocInfo struct {
|
type DocInfo struct {
|
||||||
DocID string `xml:"DocID"`
|
DocID string `xml:"DocID"`
|
||||||
Title string `xml:"Title"`
|
Title string `xml:"Title"`
|
||||||
Author string `xml:"Author"`
|
Author string `xml:"Author"`
|
||||||
Subject string `xml:"Subject"`
|
Subject string `xml:"Subject"`
|
||||||
Abstract string `xml:"Abstract"`
|
Abstract string `xml:"Abstract"`
|
||||||
CreationDate string `xml:"CreationDate"`
|
CreationDate string `xml:"CreationDate"`
|
||||||
ModDate string `xml:"ModDate"`
|
ModDate string `xml:"ModDate"`
|
||||||
CustomDatas *CustomDatas `xml:"CustomDatas"`
|
Creator string `xml:"Creator"`
|
||||||
|
CreatorVersion string `xml:"CreatorVersion"`
|
||||||
|
CustomDatas *CustomDatas `xml:"CustomDatas"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomDatas 自定义数据集合
|
// CustomDatas 自定义数据集合
|
||||||
|
|||||||
+8
-7
@@ -69,6 +69,7 @@ type pdfImporter struct {
|
|||||||
pageWidth float64
|
pageWidth float64
|
||||||
pageHeight float64
|
pageHeight float64
|
||||||
pendingPath *pdfgo.PathMark
|
pendingPath *pdfgo.PathMark
|
||||||
|
warning func(pdfgo.Diagnostic)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||||
@@ -92,6 +93,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
editor.SetRenderBackends(*options.Backends)
|
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{}}
|
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 options.OnProgress != nil {
|
||||||
if err := options.OnProgress("pages", 0, 0); err != nil {
|
if err := options.OnProgress("pages", 0, 0); err != nil {
|
||||||
return nil, PDFImportReport{}, err
|
return nil, PDFImportReport{}, err
|
||||||
@@ -126,16 +133,10 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
importer.pageHeight = height
|
importer.pageHeight = height
|
||||||
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
|
||||||
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
|
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 {
|
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
return importer.group(mark, walk, visitor)
|
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 {
|
if err := reader.WalkPage(ctx, page, visitor); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
if err := p.flushPath(); err != nil {
|
if err := p.flushPath(); err != nil {
|
||||||
return err
|
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 {
|
visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
return p.group(group, walk, visitor)
|
return p.group(group, walk, visitor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"golang.org/x/image/math/f64"
|
"golang.org/x/image/math/f64"
|
||||||
)
|
)
|
||||||
|
|
||||||
// drawRasterImage 按页面变换采样图像,两后端共享相同的高质量图像插值
|
// drawRasterImage 按页面变换对图像执行高质量采样
|
||||||
// 入参: dst 目标图像, src 源图像, m 像素坐标变换
|
// 入参: dst 目标图像, src 源图像, m 像素坐标变换
|
||||||
func drawRasterImage(dst draw.Image, src image.Image, m RasterMatrix) {
|
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)
|
draw.CatmullRom.Transform(dst, f64.Aff3{m[0], m[2], m[4], m[1], m[3], m[5]}, src, src.Bounds(), draw.Over, nil)
|
||||||
|
|||||||
@@ -56,8 +56,17 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if object.ImageMask != "" || object.Alpha != nil && *object.Alpha < 255 {
|
||||||
|
img, err = imagePixelData(img)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if object.ImageMask != "" {
|
if object.ImageMask != "" {
|
||||||
mask, err := c.renderer.ImageResource(object.ImageMask)
|
mask, err := c.renderer.ImageResource(object.ImageMask)
|
||||||
|
if err == nil {
|
||||||
|
mask, err = imagePixelData(mask)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+62
-2
@@ -16,7 +16,9 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
@@ -27,6 +29,53 @@ import (
|
|||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// creatorXMP 记录独立导出文件的制作软件
|
||||||
|
const creatorXMP = `<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/"><xmp:CreatorTool>` + ofdCreator + `</xmp:CreatorTool></rdf:Description></rdf:RDF>`
|
||||||
|
|
||||||
|
// 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画质
|
// RenderTo 按格式渲染单页,JPEG使用95画质
|
||||||
// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt
|
// 入参: page 页面内容, writer 输出流, format svg、pdf、eps、png、jpg、jpeg或txt
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -189,7 +238,13 @@ func (r *Renderer) RenderToPNG(page *PageContent, writer io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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决定
|
// RenderToJPEG 渲染为白底JPEG,像素尺寸由DPI决定
|
||||||
@@ -200,7 +255,12 @@ func (r *Renderer) RenderToJPEG(page *PageContent, writer io.Writer, options *jp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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像素
|
// fillWhiteBackground 填充白色背景,复用当前导出图片的RGBA像素
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20}
|
masks := renderCache[[32]byte, *image.Alpha]{limit: 16 << 20}
|
||||||
for i, cmd := range page.Commands {
|
for i, cmd := range page.Commands {
|
||||||
if cmd.Image != nil {
|
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 {
|
if _, linear := space.(canvas.LinearColorSpace); !linear {
|
||||||
converted := image.NewRGBA(source.Bounds())
|
converted := image.NewRGBA(source.Bounds())
|
||||||
for y := source.Bounds().Min.Y; y < source.Bounds().Max.Y; y++ {
|
for y := source.Bounds().Min.Y; y < source.Bounds().Max.Y; y++ {
|
||||||
|
|||||||
@@ -35,23 +35,38 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
|
|||||||
r.measureImage(ctx, obj, pageH, parentCTM, boundaryInCTM, parentClip)
|
r.measureImage(ctx, obj, pageH, parentCTM, boundaryInCTM, parentClip)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if obj.Visible != nil && !*obj.Visible {
|
if obj.Visible != nil && !*obj.Visible || obj.Alpha != nil && *obj.Alpha == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
r.renderError = fmt.Errorf("image resource %q not found", obj.ResourceID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
img, err := r.decodeImageResource(resPath)
|
img, err := r.decodeImageResource(resPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.renderError = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
box, _ := ParseBox(obj.Boundary)
|
box, _ := ParseBox(obj.Boundary)
|
||||||
if maskPath, ok := r.Reader.ResMap[obj.ImageMask]; ok {
|
if obj.ImageMask != "" || obj.Alpha != nil && *obj.Alpha < 255 {
|
||||||
if mask, err := r.decodeImageResource(maskPath); err == nil {
|
img, err = imagePixelData(img)
|
||||||
img = imageWithMask(img, mask)
|
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)
|
img = imageWithAlpha(img, obj.Alpha)
|
||||||
imgBounds := img.Bounds()
|
imgBounds := img.Bounds()
|
||||||
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
|
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))
|
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, localCTM, parentCTM, boundaryInCTM))
|
||||||
img = r.imageWithClip(img, clipPath, m, 96)
|
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()))
|
m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy()))
|
||||||
img, pad := imageWithTransparentEdge(img)
|
img, pad := imageWithTransparentEdge(img)
|
||||||
if pad > 0 {
|
if pad > 0 {
|
||||||
@@ -158,6 +176,12 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
|||||||
} else if clipPath.Contains(imagePath) {
|
} else if clipPath.Contains(imagePath) {
|
||||||
return img
|
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.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))))
|
scale = math.Min(scale, math.Sqrt((16<<20)/(float64(w)*float64(h))))
|
||||||
if scale > 1 {
|
if scale > 1 {
|
||||||
@@ -195,7 +219,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
|||||||
if mask.Opaque() {
|
if mask.Opaque() {
|
||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
source := imagePixelSource(img)
|
source = img
|
||||||
out := &image.NRGBA{Pix: mask.Pix, Stride: mask.Stride, Rect: bounds}
|
out := &image.NRGBA{Pix: mask.Pix, Stride: mask.Stride, Rect: bounds}
|
||||||
if src, ok := source.(*image.NRGBA); ok {
|
if src, ok := source.(*image.NRGBA); ok {
|
||||||
for y := 0; y < h; y++ {
|
for y := 0; y < h; y++ {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func (CanvasBackend) RenderSVG(r *Renderer, page *PageContent, writer io.Writer,
|
|||||||
}
|
}
|
||||||
buffer := bufio.NewWriter(writer)
|
buffer := bufio.NewWriter(writer)
|
||||||
renderer := svg.New(buffer, c.W, c.H, nil)
|
renderer := svg.New(buffer, c.W, c.H, nil)
|
||||||
|
io.WriteString(buffer, svgCreatorMetadata)
|
||||||
c.RenderTo(renderer)
|
c.RenderTo(renderer)
|
||||||
if err := renderer.Close(); err != nil {
|
if err := renderer.Close(); err != nil {
|
||||||
return SVGResources{}, err
|
return SVGResources{}, err
|
||||||
@@ -65,7 +66,7 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
|
|||||||
options := ps.DefaultOptions
|
options := ps.DefaultOptions
|
||||||
options.Format = ps.EncapsulatedPostScript
|
options.Format = ps.EncapsulatedPostScript
|
||||||
buffer := bufio.NewWriter(writer)
|
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)
|
c.RenderTo(renderer)
|
||||||
if err := renderer.Close(); err != nil {
|
if err := renderer.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -73,6 +74,24 @@ func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer)
|
|||||||
return buffer.Flush()
|
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,保留导航与文字并复用输出缓冲
|
// RenderPDF 使用canvas输出PDF,保留导航与文字并复用输出缓冲
|
||||||
// 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度
|
// 入参: r 渲染器, pages 页面列表, writer 输出流, progress 页面处理进度
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -95,7 +114,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
|||||||
if docInfo, err := r.Reader.DocInfo(); err == nil {
|
if docInfo, err := r.Reader.DocInfo(); err == nil {
|
||||||
info = *docInfo
|
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 {
|
for i, page := range pages {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
p.NewPage(page.Box.W, page.Box.H)
|
p.NewPage(page.Box.W, page.Box.H)
|
||||||
@@ -134,7 +153,7 @@ func replacePDFProducer(data []byte) []byte {
|
|||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
dst := []byte("/Producer(xiaoqidun/ofdgo)")
|
dst := []byte("/Producer(" + ofdCreator + ")")
|
||||||
copy(data[idx:idx+len(old)], dst)
|
copy(data[idx:idx+len(old)], dst)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@ import (
|
|||||||
"github.com/tdewolff/canvas/renderers/svg"
|
"github.com/tdewolff/canvas/renderers/svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// svgCreatorMetadata 记录SVG制作软件,不参与页面绘制
|
||||||
|
const svgCreatorMetadata = `<metadata>` + creatorXMP + `</metadata>`
|
||||||
|
|
||||||
// svgResourceRenderer 分离外部资源的SVG渲染器
|
// svgResourceRenderer 分离外部资源的SVG渲染器
|
||||||
type svgResourceRenderer struct {
|
type svgResourceRenderer struct {
|
||||||
*svg.SVG
|
*svg.SVG
|
||||||
@@ -69,6 +72,7 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
|||||||
writer: buffer,
|
writer: buffer,
|
||||||
seen: make(map[string]bool),
|
seen: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
io.WriteString(buffer, svgCreatorMetadata)
|
||||||
if images {
|
if images {
|
||||||
s.imageNames = make(map[image.Image]string)
|
s.imageNames = make(map[image.Image]string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ type SignatureTimestampReport struct {
|
|||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signatureTimestampEvidence 保存时间戳令牌及其绑定数据
|
||||||
type signatureTimestampEvidence struct {
|
type signatureTimestampEvidence struct {
|
||||||
Token []byte
|
Token []byte
|
||||||
Data []byte
|
Data []byte
|
||||||
|
|||||||
+2
-1
@@ -48,6 +48,7 @@ func (progress editorProgress) report(stage string, completed, total int) error
|
|||||||
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
||||||
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
||||||
|
// 制作软件统一为xiaoqidun/ofdgo,未修改的文档保留原制作软件及版本
|
||||||
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用
|
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: 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},
|
{"DocID", e.Info.DocID}, {"Title", e.Info.Title}, {"Author", e.Info.Author},
|
||||||
{"Subject", e.Info.Subject}, {"Abstract", e.Info.Abstract},
|
{"Subject", e.Info.Subject}, {"Abstract", e.Info.Abstract},
|
||||||
{"CreationDate", e.Info.CreationDate}, {"ModDate", e.Info.ModDate},
|
{"CreationDate", e.Info.CreationDate}, {"ModDate", e.Info.ModDate},
|
||||||
{"Creator", "xiaoqidun/ofdgo"},
|
{"Creator", ofdCreator},
|
||||||
} {
|
} {
|
||||||
if field[1] != "" {
|
if field[1] != "" {
|
||||||
x.text(field[0], field[1])
|
x.text(field[0], field[1])
|
||||||
|
|||||||
Reference in new issue
Block a user