feat(渲染转换): 完善字体转换与链接动作
build.yaml / build (push) Successful in 53s

This commit is contained in:
xiaoqidun committed 2026-10-02 23:38:43 +08:00
1 parent 1104f0fdd1
commit 012569dbc0
15 files changed
+380 -40

No files matched your search

+1 -1
View File
@@ -10,7 +10,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-20261002064448-3200323fdecb github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf
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
+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/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-20261002064448-3200323fdecb h1:m/ESRIQE2ssRP/bHwrJvffIz1t+QYDMzL1B2fsHI7jc= github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf h1:AjfoqpGGjeyWFH1ikxmZJ9n+hG1Qaqolaqlv9kGX91w=
github.com/xiaoqidun/pdfgo v0.0.0-20261002064448-3200323fdecb/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf/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=
+11
View File
@@ -834,6 +834,7 @@ func renderPage(args []js.Value) (any, error) {
for i, link := range page.Links { for i, link := range page.Links {
item := map[string]any{ item := map[string]any{
"uri": link.URI, "uri": link.URI,
"target": link.Target,
"path": link.Path, "path": link.Path,
"group": link.Group, "group": link.Group,
"x": link.Box.X, "x": link.Box.X,
@@ -1447,6 +1448,7 @@ func readAnnotation(args []js.Value) (any, error) {
link, err := currentEditor.AnnotationLink(args[0].Int(), annotation.ID) link, err := currentEditor.AnnotationLink(args[0].Int(), annotation.ID)
if err == nil && link.URI != nil { if err == nil && link.URI != nil {
info["linkKind"], info["linkURI"], info["linkBase"] = "uri", link.URI.URI, link.URI.Base info["linkKind"], info["linkURI"], info["linkBase"] = "uri", link.URI.URI, link.URI.Base
info["linkTarget"] = link.URI.Target
} else if err == nil && link.Goto != nil && link.Goto.Dest != nil { } else if err == nil && link.Goto != nil && link.Goto.Dest != nil {
for i, page := range currentSession.doc.Pages.Page { for i, page := range currentSession.doc.Pages.Page {
if page.ID == link.Goto.Dest.PageID { if page.ID == link.Goto.Dest.PageID {
@@ -1516,6 +1518,15 @@ func writeAnnotation(args []js.Value) (any, error) {
target = ofdgo.AnnotationLink{Dest: &ofdgo.Dest{Type: "Fit", PageID: page.ID}} target = ofdgo.AnnotationLink{Dest: &ofdgo.Dest{Type: "Fit", PageID: page.ID}}
} }
if options.Kind == "link" && options.ID != "" { if options.Kind == "link" && options.ID != "" {
if target.URI != nil {
previous, err := currentEditor.AnnotationLink(page, options.ID)
if err != nil {
return err
}
if previous.URI != nil {
target.URI.Target = previous.URI.Target
}
}
return currentEditor.UpdateAnnotationLink(page, options.ID, target) return currentEditor.UpdateAnnotationLink(page, options.ID, target)
} }
if options.Width <= 0 || options.Height <= 0 || options.Alpha < 0 || options.Alpha > 255 { if options.Width <= 0 || options.Height <= 0 || options.Alpha < 0 || options.Alpha > 255 {
+5
View File
@@ -57,19 +57,24 @@ type GotoBookmark struct {
} }
// URI URI动作 // URI URI动作
// Target保留Schema属性,不推断窗口语义
type URI struct { type URI struct {
URI string `xml:"URI,attr"` URI string `xml:"URI,attr"`
Base string `xml:"Base,attr"` Base string `xml:"Base,attr"`
Target string `xml:"Target,attr,omitempty"`
} }
// PageLink 页面点击动作,包含外链、跳转、附件和音视频播放 // PageLink 页面点击动作,包含外链、跳转、附件和音视频播放
// Box使用页面毫米坐标,Path为可选的页面坐标SVG路径,存在时作为精确点击区域 // Box使用页面毫米坐标,Path为可选的页面坐标SVG路径,存在时作为精确点击区域
// 同一来源的多个动作共享非零Group,组内按返回顺序执行 // 同一来源的多个动作共享非零Group,组内按返回顺序执行
// NewWindow用于附件动作,为空时采用OFD默认的新窗口
type PageLink struct { type PageLink struct {
URI string URI string
Target string
Box Box Box Box
Dest *Dest Dest *Dest
Attachment string Attachment string
NewWindow *bool
Path string Path string
Group int Group int
Sound *Sound Sound *Sound
+2 -1
View File
@@ -229,7 +229,7 @@ func (e *Editor) UpdateAnnotationLink(page int, id string, target AnnotationLink
} else { } else {
var check func(*editorXML) bool var check func(*editorXML) bool
check = func(node *editorXML) bool { check = func(node *editorXML) bool {
allowed := map[string]string{"URI": "URI Base", "Goto": "", "Dest": "Type PageID Left Right Top Bottom Zoom", "Bookmark": "Name"} allowed := map[string]string{"URI": "URI Base Target", "Goto": "", "Dest": "Type PageID Left Right Top Bottom Zoom", "Bookmark": "Name"}
attrs, known := allowed[node.name.Local] attrs, known := allowed[node.name.Local]
if !known || node.name.Space != root.name.Space || !editorXMLAttributes(node, attrs) { if !known || node.name.Space != root.name.Space || !editorXMLAttributes(node, attrs) {
return false return false
@@ -387,6 +387,7 @@ func annotationLinkXML(target AnnotationLink) ([]byte, error) {
if target.URI != nil { if target.URI != nil {
attrs := ofdAttrs{{Name: xml.Name{Local: "URI"}, Value: target.URI.URI}} attrs := ofdAttrs{{Name: xml.Name{Local: "URI"}, Value: target.URI.URI}}
attrs.add("Base", target.URI.Base) attrs.add("Base", target.URI.Base)
attrs.add("Target", target.URI.Target)
data, err = editorXMLContainer("URI", attrs, nil) data, err = editorXMLContainer("URI", attrs, nil)
} else { } else {
dest := target.Dest dest := target.Dest
+4
View File
@@ -147,6 +147,10 @@ func parseType1Program(data []byte) (type1Program, error) {
section = "subrs" section = "subrs"
case "/CharStrings": case "/CharStrings":
section = "glyphs" section = "glyphs"
case "def", "ND", "|-":
if section == "subrs" {
section = ""
}
case "dup": case "dup":
if section != "subrs" { if section != "subrs" {
continue continue
-1
View File
@@ -114,7 +114,6 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
return info, err return info, err
} }
info.orientation = orientation info.orientation = orientation
// PNG的物理尺寸由pHYs定义,EXIF只用于方向。
if format == "jpeg" && x > 0 && y > 0 { if format == "jpeg" && x > 0 && y > 0 {
info.xdpi, info.ydpi = x, y info.xdpi, info.ydpi = x, y
} }
+9 -5
View File
@@ -70,13 +70,14 @@ type pdfImporter struct {
fontIDs map[*pdfgo.Font]string fontIDs map[*pdfgo.Font]string
fonts map[*pdfgo.Font]*pdfImportedFont fonts map[*pdfgo.Font]*pdfImportedFont
fontWarnings map[string]bool fontWarnings map[string]bool
clipTexts map[*pdfgo.TextClip]TextObject clipTexts map[*pdfgo.TextClip][]TextObject
cmykSpace string cmykSpace string
objects []GraphicObject objects []GraphicObject
pages map[pdfgo.Reference]*pdfgo.Page pages map[pdfgo.Reference]*pdfgo.Page
pageIDs map[pdfgo.Reference]string pageIDs map[pdfgo.Reference]string
pageIndexes map[pdfgo.Reference]int pageIndexes map[pdfgo.Reference]int
imageIDs map[pdfImageKey]string imageIDs map[pdfImageKey]string
attachmentIDs map[pdfAttachmentKey]string
maskClips map[*pdfgo.SoftMask]pdfgo.Path maskClips map[*pdfgo.SoftMask]pdfgo.Path
pageBox pdfgo.Rectangle pageBox pdfgo.Rectangle
pageWidth float64 pageWidth float64
@@ -187,7 +188,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.compositeCache = nil importer.compositeCache = nil
importer.compositeNodes = nil importer.compositeNodes = nil
importer.compositeSpace = nil importer.compositeSpace = nil
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject) importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
importer.annotationData = nil importer.annotationData = nil
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil { if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return 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)
@@ -387,19 +388,21 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: p.pathData(path, box), Rule: rule}) area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: p.pathData(path, box), Rule: rule})
} }
for _, mark := range path.Text { for _, mark := range path.Text {
object, ok := p.clipTexts[mark] objects, ok := p.clipTexts[mark]
if !ok { if !ok {
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return nil, err return nil, err
} }
count := len(p.objects) count := len(p.objects)
textCount := p.report.TextObjects
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil { if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
return nil, err return nil, err
} }
p.objects = p.objects[:count] p.objects = p.objects[:count]
p.report.TextObjects-- p.report.TextObjects = textCount
object = p.clipTexts[mark] objects = p.clipTexts[mark]
} }
for _, object := range objects {
box, err := ParseBox(object.Boundary) box, err := ParseBox(object.Boundary)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -407,6 +410,7 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}) object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H})
area.Text = append(area.Text, object) area.Text = append(area.Text, object)
} }
}
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}}) clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}})
} }
return clips, nil return clips, nil
+1 -1
View File
@@ -75,7 +75,7 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
stamp.pendingPath = nil stamp.pendingPath = nil
stamp.report = PDFImportReport{} stamp.report = PDFImportReport{}
stamp.compositeCache = nil stamp.compositeCache = nil
stamp.clipTexts = make(map[*pdfgo.TextClip]TextObject) stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
stamp.pageWidth, stamp.pageHeight = box.W, box.H stamp.pageWidth, stamp.pageHeight = box.W, box.H
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error { nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
if annotation.Subtype == "Widget" { if annotation.Subtype == "Widget" {
+25 -20
View File
@@ -120,28 +120,16 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if err != nil { if err != nil {
return err return err
} }
var data []byte id, err := p.soundResource(ctx, sound)
format := "WAV" if err != nil {
if sound.File != nil { return err
var available bool }
data, available, err = p.mediaData(ctx, *sound.File) if id == "" {
if err == nil && !available {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err return err
} }
continue continue
} }
format = pdfMediaFormat(*sound.File)
} else {
data, err = pdfSoundWAV(sound)
}
if err != nil {
return err
}
id, err := p.editor.AddMedia("Audio", format, data)
if err != nil {
return err
}
if err := p.appearanceAnnotation(ctx, page, annotation, Action{Event: "CLICK", Sound: &Sound{ResourceID: id}}); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation, Action{Event: "CLICK", Sound: &Sound{ResourceID: id}}); err != nil {
return err return err
} }
@@ -180,7 +168,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
continue continue
} }
id, err := p.editor.AddAttachment(file.Name, data) id, err := p.attachmentFile(file.Name, data)
if err != nil { if err != nil {
return err return err
} }
@@ -340,14 +328,31 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre
if !ok { if !ok {
return nil, fmt.Errorf("invalid PDF link action") return nil, fmt.Errorf("invalid PDF link action")
} }
switch a["S"] { kind, err := p.reader.Resolve(a["S"])
if err != nil {
return nil, err
}
switch kind {
case pdfgo.Name("GoTo"): case pdfgo.Name("GoTo"):
target = a["D"] target = a["D"]
case pdfgo.Name("GoToR"):
return p.remoteLinkAction(a, strict)
case pdfgo.Name("Sound"):
return p.soundLinkAction(a, strict)
case pdfgo.Name("GoToE"):
converted, local, err := p.embeddedLinkAction(a, strict)
if err != nil || local == nil {
return converted, err
}
target = local
case pdfgo.Name("Named"): case pdfgo.Name("Named"):
value, err := p.reader.Resolve(a["N"]) value, err := p.reader.Resolve(a["N"])
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, ok := value.(pdfgo.Name); !ok {
return nil, fmt.Errorf("invalid PDF named action")
}
index := *currentPage index := *currentPage
switch value { switch value {
case pdfgo.Name("FirstPage"): case pdfgo.Name("FirstPage"):
@@ -359,7 +364,7 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre
case pdfgo.Name("PrevPage"): case pdfgo.Name("PrevPage"):
index = max(index-1, 0) index = max(index-1, 0)
default: default:
return nil, &pdfgo.UnsupportedError{Feature: "named link action"} return nil, nil
} }
action.Goto = &Goto{Dest: &Dest{Type: "XYZ", PageID: p.editor.pages[index].ID, OmitLeft: true, OmitTop: true, OmitZoom: true}} action.Goto = &Goto{Dest: &Dest{Type: "XYZ", PageID: p.editor.pages[index].ID, OmitLeft: true, OmitTop: true, OmitZoom: true}}
*currentPage = index *currentPage = index
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"github.com/xiaoqidun/pdfgo"
)
// pdfAttachmentKey 按显示名与原始数据复用PDF附件,不合并名称不同的附件
type pdfAttachmentKey struct {
name string
digest [32]byte
}
// pdfEmbeddedFile 保存目标路径的容器与原始文件,阅读器按需打开
type pdfEmbeddedFile struct {
reader *pdfgo.Reader
file *pdfgo.FileSpecification
data []byte
}
// remoteLinkAction 保留调用方提供的远程文件,不生成OFD无法表达的文件内目标
// 入参: object 远程跳转动作, strict 是否禁止目标语义损失
// 返回: *Action 附件动作, error 结构、取消或转换错误
func (p *pdfImporter) remoteLinkAction(object pdfgo.Object, strict bool) (*Action, error) {
action, err := p.reader.ReadExternalGoToAction(p.ctx, object)
if err != nil {
return nil, err
}
if strict {
return nil, &pdfgo.UnsupportedError{Feature: "remote document destination conversion"}
}
if p.resolveFile == nil {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF remote link file unavailable: " + action.File.Name + "; appearance retained, remote destination not transferred to OFD"})
return nil, nil
}
data, err := p.reader.ReadFileData(p.ctx, *action.File, p.resolveFile)
if err != nil {
return nil, err
}
id, err := p.attachmentFile(action.File.Name, data)
if err != nil {
return nil, err
}
message := "PDF remote link target retained as attachment; destination inside attached PDF not transferred to OFD"
if action.NewWindow == nil {
message += "; reader window preference not transferred"
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: message})
return &Action{Event: "CLICK", GotoA: &GotoA{AttachID: id, NewWindow: action.NewWindow}}, nil
}
// embeddedLinkAction 按目标路径定位附件,回到源文档时保留页内目标
// 入参: object 嵌入文件动作, strict 是否禁止附件内目标语义损失
// 返回: *Action 附件动作, pdfgo.Object 源文档目标, error 目标或转换错误
func (p *pdfImporter) embeddedLinkAction(object pdfgo.Object, strict bool) (*Action, pdfgo.Object, error) {
action, err := p.reader.ReadExternalGoToAction(p.ctx, object)
if err != nil {
return nil, nil, err
}
stack := []pdfEmbeddedFile{{reader: p.reader}}
if action.File != nil {
data, available, err := p.mediaData(p.ctx, *action.File)
if err != nil || !available {
return nil, nil, err
}
stack[0] = pdfEmbeddedFile{file: action.File, data: data}
}
var opened []*pdfgo.Reader
defer func() {
for _, reader := range opened {
reader.Close()
}
}()
open := func(file *pdfEmbeddedFile) error {
if file.reader != nil {
return nil
}
if err := p.ctx.Err(); err != nil {
return err
}
if file.data == nil {
var err error
file.data, err = p.reader.ReadFileData(p.ctx, *file.file, p.resolveFile)
if err != nil {
return err
}
}
reader, err := pdfgo.NewReaderWithOptions(bytes.NewReader(file.data), int64(len(file.data)), pdfgo.ReaderOptions{Warning: p.warning})
if err != nil {
return err
}
file.reader = reader
opened = append(opened, reader)
return p.ctx.Err()
}
for _, target := range action.Target {
if err := p.ctx.Err(); err != nil {
return nil, nil, err
}
if target.Relation == "P" {
if len(stack) == 1 {
if strict {
return nil, nil, &pdfgo.UnsupportedError{Feature: "embedded link parent document unavailable"}
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded link parent document unavailable; link omitted"})
return nil, nil, nil
}
stack = stack[:len(stack)-1]
continue
}
current := &stack[len(stack)-1]
if err := open(current); err != nil {
return nil, nil, err
}
file, err := current.reader.ReadEmbeddedTargetFile(p.ctx, target)
if err != nil {
if !strict && errors.Is(err, pdfgo.ErrDestinationNotFound) {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("%v; link omitted", err)})
return nil, nil, nil
}
return nil, nil, err
}
stack = append(stack, pdfEmbeddedFile{file: &file})
}
current := &stack[len(stack)-1]
if current.file == nil {
destination, err := current.reader.ResolveRemoteDestination(p.ctx, action.Destination)
if err != nil {
if !strict && errors.Is(err, pdfgo.ErrDestinationNotFound) {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("%v; link omitted", err)})
return nil, nil, nil
}
return nil, nil, err
}
if action.NewWindow != nil && *action.NewWindow {
if strict {
return nil, nil, &pdfgo.UnsupportedError{Feature: "embedded link to source document in new window"}
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded link new window not transferred to OFD; source document destination retained"})
}
return nil, append(pdfgo.Array{destination.Page, destination.Mode}, destination.Parameters...), nil
}
if strict {
return nil, nil, &pdfgo.UnsupportedError{Feature: "destination inside attached PDF"}
}
if current.data == nil {
current.data, err = p.reader.ReadFileData(p.ctx, *current.file, p.resolveFile)
if err != nil {
return nil, nil, err
}
}
id, err := p.attachmentFile(current.file.Name, current.data)
if err != nil {
return nil, nil, err
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded link target retained as attachment; destination inside attached PDF not transferred to OFD"})
return &Action{Event: "CLICK", GotoA: &GotoA{AttachID: id, NewWindow: action.NewWindow}}, nil, nil
}
// attachmentFile 添加或复用PDF附件,缺少显示名时使用固定名称,不改原始文件内容
// 入参: name 附件显示名, data 原始文件数据
// 返回: string 附件标识, error 附件写入错误
func (p *pdfImporter) attachmentFile(name string, data []byte) (string, error) {
if name == "" {
name = "Attachment"
}
key := pdfAttachmentKey{name: name, digest: sha256.Sum256(data)}
if id, ok := p.attachmentIDs[key]; ok {
return id, nil
}
id, err := p.editor.AddAttachment(name, data)
if err != nil {
return "", err
}
if p.attachmentIDs == nil {
p.attachmentIDs = make(map[pdfAttachmentKey]string)
}
p.attachmentIDs[key] = id
return id, nil
}
+62
View File
@@ -27,6 +27,68 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// soundResource 注册采样音频或调用方提供的外部音频,不自动访问文件
// 入参: ctx 取消上下文, sound 音频对象
// 返回: string 资源标识,不可用时为空, error 读取或封装错误
func (p *pdfImporter) soundResource(ctx context.Context, sound pdfgo.Sound) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
var data []byte
var err error
format := "WAV"
if sound.File != nil {
var available bool
data, available, err = p.mediaData(ctx, *sound.File)
if err != nil || !available {
return "", err
}
format = pdfMediaFormat(*sound.File)
} else {
data, err = pdfSoundWAV(sound)
}
if err != nil {
return "", err
}
if err := ctx.Err(); err != nil {
return "", err
}
return p.editor.AddMedia("Audio", format, data)
}
// soundLinkAction 保留音频点击动作,混音、交互限制及音量损失按策略报告
// 入参: object 音频动作, strict 是否禁止播放参数损失
// 返回: *Action 音频动作, error 结构、参数或资源错误
func (p *pdfImporter) soundLinkAction(object pdfgo.Object, strict bool) (*Action, error) {
source, err := p.reader.ReadSoundAction(p.ctx, object)
if err != nil {
return nil, err
}
if source.Volume < 0 {
return nil, &pdfgo.UnsupportedError{Feature: "negative sound action volume conversion"}
}
if strict {
return nil, &pdfgo.UnsupportedError{Feature: "sound action playback policy conversion"}
}
volume := int(math.Round(source.Volume * 100))
if source.Mix {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF sound action mixing policy not transferred to OFD"})
} else {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF sound action exclusive playback policy not transferred to OFD"})
}
if source.Synchronous {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF sound action interaction restriction not transferred to OFD; action waiting retained"})
}
if float64(volume)/100 != source.Volume {
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF sound action volume rounded to OFD integer volume"})
}
id, err := p.soundResource(p.ctx, source.Sound)
if err != nil || id == "" {
return nil, err
}
return &Action{Event: "CLICK", Sound: &Sound{ResourceID: id, Volume: &volume, Repeat: source.Repeat, Synchronous: source.Synchronous}}, nil
}
// movieAnnotation 转换内嵌视频及其点击播放动作,保留独立海报外观 // movieAnnotation 转换内嵌视频及其点击播放动作,保留独立海报外观
// 入参: ctx 取消上下文, page PDF页面, annotation 视频注解 // 入参: ctx 取消上下文, page PDF页面, annotation 视频注解
// 返回: error 媒体资源或播放参数无法表示时返回错误 // 返回: error 媒体资源或播放参数无法表示时返回错误
+55 -7
View File
@@ -199,6 +199,19 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
return p.flushPath() return p.flushPath()
} }
if mark.Size == 0 || mark.HorizontalScale == 0 {
return p.degenerateText(mark)
}
if mark.Size < 0 || mark.HorizontalScale < 0 {
x, y := math.Copysign(1, mark.Size*mark.HorizontalScale), math.Copysign(1, mark.Size)
mark.Matrix = mark.Matrix.Mul(pdfgo.Matrix{x, 0, 0, y, 0, 0})
mark.Size, mark.HorizontalScale = math.Abs(mark.Size), math.Abs(mark.HorizontalScale)
positions := make([]pdfgo.Point, len(mark.Positions))
for index, position := range mark.Positions {
positions[index] = pdfgo.Point{X: position.X * x, Y: position.Y * y}
}
mark.Positions = positions
}
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err return err
} }
@@ -291,11 +304,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
gid := glyph.ID gid := glyph.ID
if embedded && font.ProgramType == "FontFile" { if embedded && font.ProgramType == "FontFile" {
var found bool gid = imported.type1Glyphs[glyph.Name]
gid, found = imported.type1Glyphs[glyph.Name]
if !found {
return fmt.Errorf("missing Type1 glyph %q in PDF font %s", glyph.Name, font.Name)
}
glyph.HasID = true glyph.HasID = true
} }
if glyph.Text == "" && glyph.HasID { if glyph.Text == "" && glyph.HasID {
@@ -433,9 +442,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
*clipObject.Stroke = false *clipObject.Stroke = false
clipObject.Visible = nil clipObject.Visible = nil
if p.clipTexts == nil { if p.clipTexts == nil {
p.clipTexts = make(map[*pdfgo.TextClip]TextObject) p.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
} }
p.clipTexts[mark.Clip] = clipObject p.clipTexts[mark.Clip] = []TextObject{clipObject}
} }
if outlineStroke { if outlineStroke {
for _, path := range glyphPaths { for _, path := range glyphPaths {
@@ -460,3 +469,42 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
p.report.TextObjects++ p.report.TextObjects++
return nil return nil
} }
// degenerateText 保留零字号或零水平缩放的逐字位置与退化字形
// 入参: mark PDF文字绘制信息
// 返回: error 转换错误
func (p *pdfImporter) degenerateText(mark pdfgo.TextMark) error {
x, y := math.Copysign(1, mark.Size*mark.HorizontalScale), math.Copysign(1, mark.Size)
size, scale := math.Abs(mark.Size), math.Abs(mark.HorizontalScale)
if size == 0 {
size, x, y = 1, 0, 0
}
if scale == 0 {
scale, x = 1, 0
}
var clips []TextObject
for index, position := range mark.Positions {
glyph := mark
glyph.Size, glyph.HorizontalScale = size, scale
glyph.Matrix = mark.Matrix.Mul(pdfgo.Matrix{1, 0, 0, 1, position.X, position.Y}).Mul(pdfgo.Matrix{x, 0, 0, y, 0, 0})
glyph.Positions = []pdfgo.Point{{}}
glyph.Glyphs = mark.Glyphs[index : index+1]
if mark.Clip != nil {
glyph.Clip = new(pdfgo.TextClip)
}
if err := p.text(glyph); err != nil {
return err
}
if glyph.Clip != nil {
clips = append(clips, p.clipTexts[glyph.Clip]...)
delete(p.clipTexts, glyph.Clip)
}
}
if mark.Clip != nil {
if p.clipTexts == nil {
p.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
}
p.clipTexts[mark.Clip] = clips
}
return nil
}
+5
View File
@@ -88,9 +88,14 @@ func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
} }
} else if action.URI != nil && action.URI.URI != "" { } else if action.URI != nil && action.URI.URI != "" {
link.URI = resolveActionURI(*action.URI) link.URI = resolveActionURI(*action.URI)
link.Target = action.URI.Target
links = append(links, link) links = append(links, link)
} else if action.GotoA != nil { } else if action.GotoA != nil {
link.Attachment = action.GotoA.AttachID link.Attachment = action.GotoA.AttachID
if action.GotoA.NewWindow != nil {
window := *action.GotoA.NewWindow
link.NewWindow = &window
}
links = append(links, link) links = append(links, link)
} else if action.Sound != nil { } else if action.Sound != nil {
value := *action.Sound value := *action.Sound
-2
View File
@@ -114,7 +114,6 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
_, height := s.Size() _, height := s.Size()
size := img.Bounds().Size() size := img.Bounds().Size()
m = m.Translate(0, float64(size.Y)) m = m.Translate(0, float64(size.Y))
// 直接写出换轴后的矩阵,原坐标平移为零时仍需保留页面高度补偿。
matrix := Matrix{a: m[0][0], b: -m[1][0], c: -m[0][1], d: m[1][1], e: m[0][2], f: height - m[1][2]} matrix := Matrix{a: m[0][0], b: -m[1][0], c: -m[0][1], d: m[1][1], e: m[0][2], f: height - m[1][2]}
fmt.Fprintf(s.writer, `<image transform="matrix(%s)" width="%d" height="%d" xlink:href="`, matrix.String(), size.X, size.Y) fmt.Fprintf(s.writer, `<image transform="matrix(%s)" width="%d" height="%d" xlink:href="`, matrix.String(), size.X, size.Y)
if s.imageNames != nil { if s.imageNames != nil {
@@ -187,7 +186,6 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
seen: make(map[string]bool), seen: make(map[string]bool),
} }
io.WriteString(buffer, svgCreatorMetadata) io.WriteString(buffer, svgCreatorMetadata)
// 图片方向由文档变换决定,不再由浏览器应用EXIF方向。
io.WriteString(buffer, `<g style="image-orientation:none">`) io.WriteString(buffer, `<g style="image-orientation:none">`)
if images { if images {
s.imageNames = make(map[svgImageKey]string) s.imageNames = make(map[svgImageKey]string)