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
+390 -50

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/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
+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-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=
+11
View File
@@ -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 {
+7 -2
View File
@@ -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
+2 -1
View File
@@ -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
+4
View File
@@ -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
-1
View File
@@ -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
}
+14 -10
View File
@@ -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}})
}
+1 -1
View File
@@ -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" {
+28 -23
View File
@@ -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
+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"
)
// 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 媒体资源或播放参数无法表示时返回错误
+55 -7
View File
@@ -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
}
+5
View File
@@ -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
-2
View File
@@ -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, `<image transform="matrix(%s)" width="%d" height="%d" xlink:href="`, matrix.String(), size.X, size.Y)
if s.imageNames != nil {
@@ -187,7 +186,6 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
seen: make(map[string]bool),
}
io.WriteString(buffer, svgCreatorMetadata)
// 图片方向由文档变换决定,不再由浏览器应用EXIF方向。
io.WriteString(buffer, `<g style="image-orientation:none">`)
if images {
s.imageNames = make(map[svgImageKey]string)