mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1 parent
1104f0fdd1
commit
012569dbc0
15 files changed
+390
-50
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+7
-2
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-10
@@ -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,25 +388,28 @@ 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]
|
||||||
}
|
}
|
||||||
box, err := ParseBox(object.Boundary)
|
for _, object := range objects {
|
||||||
if err != nil {
|
box, err := ParseBox(object.Boundary)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H})
|
||||||
|
area.Text = append(area.Text, object)
|
||||||
}
|
}
|
||||||
object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H})
|
|
||||||
area.Text = append(area.Text, object)
|
|
||||||
}
|
}
|
||||||
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}})
|
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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" {
|
||||||
|
|||||||
+28
-23
@@ -120,27 +120,15 @@ 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
|
|
||||||
}
|
}
|
||||||
format = pdfMediaFormat(*sound.File)
|
continue
|
||||||
} 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user