diff --git a/go.mod b/go.mod index 6f6837c..ba4917c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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-20261002064448-3200323fdecb + github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 54c3b89..98e3931 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20261002064448-3200323fdecb h1:m/ESRIQE2ssRP/bHwrJvffIz1t+QYDMzL1B2fsHI7jc= -github.com/xiaoqidun/pdfgo v0.0.0-20261002064448-3200323fdecb/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf h1:AjfoqpGGjeyWFH1ikxmZJ9n+hG1Qaqolaqlv9kGX91w= +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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index d2d388b..320f930 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -834,6 +834,7 @@ func renderPage(args []js.Value) (any, error) { for i, link := range page.Links { item := map[string]any{ "uri": link.URI, + "target": link.Target, "path": link.Path, "group": link.Group, "x": link.Box.X, @@ -1447,6 +1448,7 @@ func readAnnotation(args []js.Value) (any, error) { link, err := currentEditor.AnnotationLink(args[0].Int(), annotation.ID) if err == nil && link.URI != nil { 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 { for i, page := range currentSession.doc.Pages.Page { 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}} } 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) } if options.Width <= 0 || options.Height <= 0 || options.Alpha < 0 || options.Alpha > 255 { diff --git a/ofdgo_action.go b/ofdgo_action.go index 401e014..c85bb58 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -57,19 +57,24 @@ type GotoBookmark struct { } // URI URI动作 +// Target保留Schema属性,不推断窗口语义 type URI struct { - URI string `xml:"URI,attr"` - Base string `xml:"Base,attr"` + URI string `xml:"URI,attr"` + Base string `xml:"Base,attr"` + Target string `xml:"Target,attr,omitempty"` } // PageLink 页面点击动作,包含外链、跳转、附件和音视频播放 // Box使用页面毫米坐标,Path为可选的页面坐标SVG路径,存在时作为精确点击区域 // 同一来源的多个动作共享非零Group,组内按返回顺序执行 +// NewWindow用于附件动作,为空时采用OFD默认的新窗口 type PageLink struct { URI string + Target string Box Box Dest *Dest Attachment string + NewWindow *bool Path string Group int Sound *Sound diff --git a/ofdgo_editor_link.go b/ofdgo_editor_link.go index 8b260f6..8e6b802 100644 --- a/ofdgo_editor_link.go +++ b/ofdgo_editor_link.go @@ -229,7 +229,7 @@ func (e *Editor) UpdateAnnotationLink(page int, id string, target AnnotationLink } else { var check func(*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] if !known || node.name.Space != root.name.Space || !editorXMLAttributes(node, attrs) { return false @@ -387,6 +387,7 @@ func annotationLinkXML(target AnnotationLink) ([]byte, error) { if target.URI != nil { attrs := ofdAttrs{{Name: xml.Name{Local: "URI"}, Value: target.URI.URI}} attrs.add("Base", target.URI.Base) + attrs.add("Target", target.URI.Target) data, err = editorXMLContainer("URI", attrs, nil) } else { dest := target.Dest diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go index 906d4d1..49b5903 100644 --- a/ofdgo_font_type1.go +++ b/ofdgo_font_type1.go @@ -147,6 +147,10 @@ func parseType1Program(data []byte) (type1Program, error) { section = "subrs" case "/CharStrings": section = "glyphs" + case "def", "ND", "|-": + if section == "subrs" { + section = "" + } case "dup": if section != "subrs" { continue diff --git a/ofdgo_image_metadata.go b/ofdgo_image_metadata.go index 3949774..9153711 100644 --- a/ofdgo_image_metadata.go +++ b/ofdgo_image_metadata.go @@ -114,7 +114,6 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, return info, err } info.orientation = orientation - // PNG的物理尺寸由pHYs定义,EXIF只用于方向。 if format == "jpeg" && x > 0 && y > 0 { info.xdpi, info.ydpi = x, y } diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index c5aee69..119215f 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -70,13 +70,14 @@ type pdfImporter struct { fontIDs map[*pdfgo.Font]string fonts map[*pdfgo.Font]*pdfImportedFont fontWarnings map[string]bool - clipTexts map[*pdfgo.TextClip]TextObject + clipTexts map[*pdfgo.TextClip][]TextObject cmykSpace string objects []GraphicObject pages map[pdfgo.Reference]*pdfgo.Page pageIDs map[pdfgo.Reference]string pageIndexes map[pdfgo.Reference]int imageIDs map[pdfImageKey]string + attachmentIDs map[pdfAttachmentKey]string maskClips map[*pdfgo.SoftMask]pdfgo.Path pageBox pdfgo.Rectangle pageWidth float64 @@ -187,7 +188,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI importer.compositeCache = nil importer.compositeNodes = nil importer.compositeSpace = nil - importer.clipTexts = make(map[*pdfgo.TextClip]TextObject) + importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject) importer.annotationData = 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) @@ -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}) } for _, mark := range path.Text { - object, ok := p.clipTexts[mark] + objects, ok := p.clipTexts[mark] if !ok { if err := p.flushPath(); err != nil { return nil, err } 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 { return nil, err } p.objects = p.objects[:count] - p.report.TextObjects-- - object = p.clipTexts[mark] + p.report.TextObjects = textCount + objects = p.clipTexts[mark] } - box, err := ParseBox(object.Boundary) - if err != nil { - return nil, err + for _, object := range objects { + box, err := ParseBox(object.Boundary) + 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}}) } diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index 8699ca9..996f197 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -75,7 +75,7 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page stamp.pendingPath = nil stamp.report = PDFImportReport{} 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 nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error { if annotation.Subtype == "Widget" { diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index 7fd3213..8b09add 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -120,27 +120,15 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict if err != nil { return err } - var data []byte - format := "WAV" - if sound.File != nil { - var available bool - data, available, err = p.mediaData(ctx, *sound.File) - if err == nil && !available { - if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { - return err - } - continue + id, err := p.soundResource(ctx, sound) + if err != nil { + return err + } + if id == "" { + if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { + return err } - 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 + continue } if err := p.appearanceAnnotation(ctx, page, annotation, Action{Event: "CLICK", Sound: &Sound{ResourceID: id}}); err != nil { return err @@ -180,7 +168,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict } continue } - id, err := p.editor.AddAttachment(file.Name, data) + id, err := p.attachmentFile(file.Name, data) if err != nil { return err } @@ -340,14 +328,31 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre if !ok { 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"): 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"): value, err := p.reader.Resolve(a["N"]) if err != nil { return nil, err } + if _, ok := value.(pdfgo.Name); !ok { + return nil, fmt.Errorf("invalid PDF named action") + } index := *currentPage switch value { case pdfgo.Name("FirstPage"): @@ -359,7 +364,7 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre case pdfgo.Name("PrevPage"): index = max(index-1, 0) 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}} *currentPage = index diff --git a/ofdgo_pdf_link_file.go b/ofdgo_pdf_link_file.go new file mode 100644 index 0000000..2f8fabb --- /dev/null +++ b/ofdgo_pdf_link_file.go @@ -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 +} diff --git a/ofdgo_pdf_media.go b/ofdgo_pdf_media.go index 0ed7be3..4e04015 100644 --- a/ofdgo_pdf_media.go +++ b/ofdgo_pdf_media.go @@ -27,6 +27,68 @@ import ( "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 转换内嵌视频及其点击播放动作,保留独立海报外观 // 入参: ctx 取消上下文, page PDF页面, annotation 视频注解 // 返回: error 媒体资源或播放参数无法表示时返回错误 diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index df5aee3..51b7dc9 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -199,6 +199,19 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } 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 { return err } @@ -291,11 +304,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } gid := glyph.ID if embedded && font.ProgramType == "FontFile" { - var found bool - gid, found = imported.type1Glyphs[glyph.Name] - if !found { - return fmt.Errorf("missing Type1 glyph %q in PDF font %s", glyph.Name, font.Name) - } + gid = imported.type1Glyphs[glyph.Name] glyph.HasID = true } if glyph.Text == "" && glyph.HasID { @@ -433,9 +442,9 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { *clipObject.Stroke = false clipObject.Visible = 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 { for _, path := range glyphPaths { @@ -460,3 +469,42 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { p.report.TextObjects++ 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 +} diff --git a/ofdgo_render.go b/ofdgo_render.go index e432989..b68c664 100644 --- a/ofdgo_render.go +++ b/ofdgo_render.go @@ -88,9 +88,14 @@ func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) { } } else if action.URI != nil && action.URI.URI != "" { link.URI = resolveActionURI(*action.URI) + link.Target = action.URI.Target links = append(links, link) } else if action.GotoA != nil { link.Attachment = action.GotoA.AttachID + if action.GotoA.NewWindow != nil { + window := *action.GotoA.NewWindow + link.NewWindow = &window + } links = append(links, link) } else if action.Sound != nil { value := *action.Sound diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index 9eccd56..8aefacb 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -114,7 +114,6 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) { _, height := s.Size() size := img.Bounds().Size() 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]} fmt.Fprintf(s.writer, ``) if images { s.imageNames = make(map[svgImageKey]string)