feat(转换编辑): 优化转换与编辑功能
build.yaml / build (push) Successful in 39s

This commit is contained in:
xiaoqidun committed 2026-09-26 10:50:52 +08:00
1 parent 35631cdf98
commit c3908819bb
23 files changed
+294 -72

No files matched your search

+5 -1
View File
@@ -573,6 +573,10 @@
<dt>修改</dt>
<dd id="metaModDate"></dd>
</div>
<div hidden>
<dt>软件</dt>
<dd id="metaCreator"></dd>
</div>
<div>
<dt>类型</dt>
<dd id="metaType">-</dd>
@@ -591,7 +595,7 @@
</div>
</dl>
<details id="conversionNotice" class="conversion-notice" hidden>
<summary>转换提示</summary>
<summary>提示</summary>
<div id="conversionWarnings" class="attachment-detail"></div>
</details>
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
+22 -8
View File
@@ -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;
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+4
View File
@@ -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 {
+8 -11
View File
@@ -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
+41 -11
View File
@@ -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 编码错误
+3 -2
View File
@@ -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 {
+18
View File
@@ -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)
+21 -7
View File
@@ -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 {
+21 -8
View File
@@ -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 {
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 灰度像素
+5
View File
@@ -16,6 +16,9 @@ package ofdgo
import "encoding/xml"
// ofdCreator 标识本库生成的文档
const ofdCreator = "xiaoqidun/ofdgo"
// OFD OFD入口结构
// 代表OFD文档的根节点
type OFD struct {
@@ -42,6 +45,8 @@ type DocInfo struct {
Abstract string `xml:"Abstract"`
CreationDate string `xml:"CreationDate"`
ModDate string `xml:"ModDate"`
Creator string `xml:"Creator"`
CreatorVersion string `xml:"CreatorVersion"`
CustomDatas *CustomDatas `xml:"CustomDatas"`
}
+8 -7
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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)
+9
View File
@@ -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
}
+62 -2
View File
@@ -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 = `<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画质
// 入参: 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像素
+4 -1
View File
@@ -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++ {
+29 -5
View File
@@ -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++ {
+22 -3
View File
@@ -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
}
+4
View File
@@ -30,6 +30,9 @@ import (
"github.com/tdewolff/canvas/renderers/svg"
)
// svgCreatorMetadata 记录SVG制作软件,不参与页面绘制
const svgCreatorMetadata = `<metadata>` + creatorXMP + `</metadata>`
// 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)
}
+1
View File
@@ -76,6 +76,7 @@ type SignatureTimestampReport struct {
Error string
}
// signatureTimestampEvidence 保存时间戳令牌及其绑定数据
type signatureTimestampEvidence struct {
Token []byte
Data []byte
+2 -1
View File
@@ -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])