feat(转换能力): 完善链接与图层转换
build.yaml / build (push) Successful in 41s

This commit is contained in:
xiaoqidun committed 2026-09-28 19:15:12 +08:00
1 parent 035bbfb079
commit 73105646aa
4 files changed
+150 -95

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20260928103444-c1e05032dc3a
github.com/xiaoqidun/pdfgo v0.0.0-20260928111239-49265df4c04c
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-20260928103444-c1e05032dc3a h1:ROTJKhSKiF4LHufA5UjBWwGQimvWQV8hU9SxsrkeTWE=
github.com/xiaoqidun/pdfgo v0.0.0-20260928103444-c1e05032dc3a/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260928111239-49265df4c04c h1:AkN9yw2b5LXinOBkWy/Wppdu4AtqQ2j6M4AYxlThL/A=
github.com/xiaoqidun/pdfgo v0.0.0-20260928111239-49265df4c04c/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=
+3
View File
@@ -75,6 +75,7 @@ type pdfImporter struct {
objects []GraphicObject
pages map[pdfgo.Reference]*pdfgo.Page
pageIDs map[pdfgo.Reference]string
pageIndexes map[pdfgo.Reference]int
imageIDs map[pdfImageKey]string
maskClips map[*pdfgo.SoftMask]pdfgo.Path
pageBox pdfgo.Rectangle
@@ -128,6 +129,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
editor.fontDirs, editor.fontFS = renderer.FontSources()
importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}}
importer.report.Warnings = diagnostics
importer.pageIndexes = make(map[pdfgo.Reference]int)
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
if options.Strict {
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"}
@@ -152,6 +154,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
}
importer.pages[page.Reference] = page
importer.pageIDs[page.Reference] = editor.pages[index].ID
importer.pageIndexes[page.Reference] = index
if options.OnProgress != nil {
return options.OnProgress("pages", index+1, 0)
}
+144 -92
View File
@@ -65,6 +65,22 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
continue
}
dict := annotation.Dictionary
if dict["OC"] != nil {
if strict {
return &pdfgo.UnsupportedError{Feature: "annotation optional content conversion"}
}
visible, err := p.reader.OptionalContentVisible(dict["OC"])
if err != nil {
return err
}
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF annotation optional content default visibility applied; layer switching not transferred"})
if !visible {
continue
}
dict = maps.Clone(dict)
delete(dict, "OC")
annotation.Dictionary = dict
}
if annotation.Subtype == "Text" {
value, err := p.reader.Resolve(dict["Open"])
if err != nil {
@@ -169,24 +185,121 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if annotation.Subtype != "Link" {
return &pdfgo.UnsupportedError{Feature: "annotation " + string(annotation.Subtype)}
}
for _, key := range []pdfgo.Name{"AA", "OC"} {
if dict[key] != nil {
return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("link annotation field %q", key)}
if dict["AA"] != nil {
return &pdfgo.UnsupportedError{Feature: "link annotation field \"AA\""}
}
}
action := Action{Event: "CLICK"}
target := dict["Dest"]
if dict["A"] != nil {
value, err := p.reader.Resolve(dict["A"])
var actions []Action
currentPage := p.page
appendAction := func(dict pdfgo.Dictionary) error {
current := annotation
current.Dictionary = dict
action, err := p.linkAction(current, strict, &currentPage)
if err != nil {
return err
}
if action != nil {
actions = append(actions, *action)
}
return nil
}
if dict["A"] == nil {
if err := appendAction(dict); err != nil {
return err
}
} else if err := p.reader.WalkActions(ctx, dict["A"], func(info pdfgo.ActionInfo) error {
current := maps.Clone(dict)
current["A"] = info.Dictionary
return appendAction(current)
}); err != nil {
return err
}
b := annotation.Rect
box := pdfBounds([]pdfgo.Point{p.matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), p.matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax})})
region, err := p.linkRegion(annotation, box)
if err != nil {
return err
}
for i := range actions {
actions[i].Region = region
}
if err := p.appearanceAnnotation(ctx, page, annotation, actions...); err != nil {
return err
}
if len(actions) != 0 {
p.report.Links++
}
}
return nil
}
// linkRegion 将PDF链接四边形映射为OFD动作区域,越出注解矩形时使用矩形
// 入参: annotation PDF链接注解, box OFD对象边界
// 返回: *Region 点击区域, error 解析错误
func (p *pdfImporter) linkRegion(annotation pdfgo.Annotation, box Box) (*Region, error) {
value, err := p.reader.Resolve(annotation.Dictionary["QuadPoints"])
if err != nil || value == nil {
return nil, err
}
array, ok := value.(pdfgo.Array)
if !ok || len(array) == 0 || len(array)%8 != 0 {
return nil, nil
}
region := &Region{Area: make([]RegionArea, 0, len(array)/8)}
for start := 0; start < len(array); start += 8 {
var points [4]pdfgo.Point
for index := range points {
coordinates := [2]float64{}
for axis := range coordinates {
resolved, err := p.reader.Resolve(array[start+index*2+axis])
if err != nil {
return nil, err
}
switch number := resolved.(type) {
case pdfgo.Integer:
coordinates[axis] = float64(number)
case pdfgo.Real:
coordinates[axis] = float64(number)
default:
return nil, nil
}
}
x, y := coordinates[0], coordinates[1]
if !finite(x) || !finite(y) || x < annotation.Rect.XMin || x > annotation.Rect.XMax || y < annotation.Rect.YMin || y > annotation.Rect.YMax {
return nil, nil
}
points[index] = p.matrix.Apply(pdfgo.Point{X: x, Y: y})
}
area := RegionArea{Start: pdfNumbers(points[0].X-box.X, points[0].Y-box.Y)}
for _, point := range points[1:] {
area.Command = append(area.Command, RegionCommand{Type: "Line", Point1: pdfNumbers(point.X-box.X, point.Y-box.Y)})
}
region.Area = append(region.Area, area)
}
return region, nil
}
// linkAction 将单个链接动作转换为OFD动作,失效目标按导入策略报告
// 入参: annotation 链接注解, strict 严格检查开关, currentPage 动作链当前页
// 返回: *Action 动作,失效目标为空, error 转换错误
func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, currentPage *int) (*Action, error) {
dict := annotation.Dictionary
action := Action{Event: "CLICK"}
target, err := p.reader.Resolve(dict["Dest"])
if err != nil {
return nil, err
}
if target == nil && dict["A"] == nil {
return nil, nil
}
if dict["A"] != nil {
value, err := p.reader.Resolve(dict["A"])
if err != nil {
return nil, err
}
a, ok := value.(pdfgo.Dictionary)
if !ok {
return fmt.Errorf("invalid PDF link action")
}
if a["Next"] != nil {
return &pdfgo.UnsupportedError{Feature: "chained link actions"}
return nil, fmt.Errorf("invalid PDF link action")
}
switch a["S"] {
case pdfgo.Name("GoTo"):
@@ -194,9 +307,9 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
case pdfgo.Name("Named"):
value, err := p.reader.Resolve(a["N"])
if err != nil {
return err
return nil, err
}
index := p.page
index := *currentPage
switch value {
case pdfgo.Name("FirstPage"):
index = 0
@@ -207,35 +320,36 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
case pdfgo.Name("PrevPage"):
index = max(index-1, 0)
default:
return &pdfgo.UnsupportedError{Feature: "named link action"}
return nil, &pdfgo.UnsupportedError{Feature: "named link action"}
}
action.Goto = &Goto{Dest: &Dest{Type: "XYZ", PageID: p.editor.pages[index].ID, OmitLeft: true, OmitTop: true, OmitZoom: true}}
*currentPage = index
case pdfgo.Name("URI"):
value, err := p.reader.Resolve(a["URI"])
if err != nil {
return err
return nil, err
}
uri, ok := value.(pdfgo.String)
if !ok {
return fmt.Errorf("invalid PDF URI")
return nil, fmt.Errorf("invalid PDF URI")
}
isMap, err := p.reader.Resolve(a["IsMap"])
if err != nil {
return err
return nil, err
}
if isMap != nil && isMap != pdfgo.Boolean(false) && isMap != pdfgo.Boolean(true) {
return fmt.Errorf("invalid PDF URI image map flag")
return nil, fmt.Errorf("invalid PDF URI image map flag")
}
if isMap == pdfgo.Boolean(true) {
return &pdfgo.UnsupportedError{Feature: "URI image map"}
return nil, &pdfgo.UnsupportedError{Feature: "URI image map"}
}
base, err := p.reader.BaseURI()
if err != nil {
return err
return nil, err
}
action.URI = &URI{URI: string(uri), Base: base}
default:
return &pdfgo.UnsupportedError{Feature: "link action"}
return nil, &pdfgo.UnsupportedError{Feature: "link action"}
}
}
if action.URI == nil && action.Goto == nil {
@@ -243,23 +357,17 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if err != nil {
if !strict && errors.Is(err, pdfgo.ErrDestinationNotFound) {
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: fmt.Sprintf("%v; link omitted", err)})
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
return nil, nil
}
continue
}
return err
return nil, err
}
targetPage := p.pages[destination.Page]
if targetPage == nil {
if !strict {
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF link targets a page outside the document page tree; link omitted"})
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
return nil, nil
}
continue
}
return fmt.Errorf("PDF destination page missing")
return nil, fmt.Errorf("PDF destination page missing")
}
matrix, _, _ := pdfPageMatrix(targetPage)
dest := &Dest{Type: string(destination.Mode), PageID: p.pageIDs[destination.Page]}
@@ -317,7 +425,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
case pdfgo.Real:
values[n] = float64(v)
default:
return fmt.Errorf("invalid PDF FitR destination coordinate")
return nil, fmt.Errorf("invalid PDF FitR destination coordinate")
}
}
bounds := pdfBounds([]pdfgo.Point{
@@ -327,66 +435,10 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
dest.Left, dest.Top = bounds.X, bounds.Y
dest.Right, dest.Bottom = bounds.X+bounds.W, bounds.Y+bounds.H
} else if destination.Mode != "Fit" {
return &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)}
return nil, &pdfgo.UnsupportedError{Feature: "destination mode " + string(destination.Mode)}
}
action.Goto = &Goto{Dest: dest}
*currentPage = p.pageIndexes[destination.Page]
}
b := annotation.Rect
box := pdfBounds([]pdfgo.Point{p.matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), p.matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax})})
action.Region, err = p.linkRegion(annotation, box)
if err != nil {
return err
}
if err := p.appearanceAnnotation(ctx, page, annotation, action); err != nil {
return err
}
p.report.Links++
}
return nil
}
// linkRegion 将PDF链接四边形映射为OFD动作区域,越出注解矩形时使用矩形
// 入参: annotation PDF链接注解, box OFD对象边界
// 返回: *Region 点击区域, error 解析错误
func (p *pdfImporter) linkRegion(annotation pdfgo.Annotation, box Box) (*Region, error) {
value, err := p.reader.Resolve(annotation.Dictionary["QuadPoints"])
if err != nil || value == nil {
return nil, err
}
array, ok := value.(pdfgo.Array)
if !ok || len(array) == 0 || len(array)%8 != 0 {
return nil, nil
}
region := &Region{Area: make([]RegionArea, 0, len(array)/8)}
for start := 0; start < len(array); start += 8 {
var points [4]pdfgo.Point
for index := range points {
coordinates := [2]float64{}
for axis := range coordinates {
resolved, err := p.reader.Resolve(array[start+index*2+axis])
if err != nil {
return nil, err
}
switch number := resolved.(type) {
case pdfgo.Integer:
coordinates[axis] = float64(number)
case pdfgo.Real:
coordinates[axis] = float64(number)
default:
return nil, nil
}
}
x, y := coordinates[0], coordinates[1]
if !finite(x) || !finite(y) || x < annotation.Rect.XMin || x > annotation.Rect.XMax || y < annotation.Rect.YMin || y > annotation.Rect.YMax {
return nil, nil
}
points[index] = p.matrix.Apply(pdfgo.Point{X: x, Y: y})
}
area := RegionArea{Start: pdfNumbers(points[0].X-box.X, points[0].Y-box.Y)}
for _, point := range points[1:] {
area.Command = append(area.Command, RegionCommand{Type: "Line", Point1: pdfNumbers(point.X-box.X, point.Y-box.Y)})
}
region.Area = append(region.Area, area)
}
return region, nil
return &action, nil
}