mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
323 lines
9.2 KiB
Go
323 lines
9.2 KiB
Go
// 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 (
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/tdewolff/canvas"
|
|
"github.com/tdewolff/canvas/renderers/pdf"
|
|
)
|
|
|
|
// pdfPage PDF页面数据
|
|
type pdfPage struct {
|
|
Content *PageContent
|
|
Box Box
|
|
}
|
|
|
|
// pdfNavigation PDF导航信息
|
|
type pdfNavigation struct {
|
|
Anchor map[int][]pdfAnchor
|
|
Link map[int][]pdfLink
|
|
Outline map[int][]pdfOutline
|
|
nextID int
|
|
}
|
|
|
|
// pdfAnchor PDF跳转目标
|
|
type pdfAnchor struct {
|
|
Name string
|
|
Rect canvas.Rect
|
|
}
|
|
|
|
// pdfLink PDF链接
|
|
type pdfLink struct {
|
|
URI string
|
|
Rect canvas.Rect
|
|
}
|
|
|
|
// pdfOutline PDF大纲
|
|
type pdfOutline struct {
|
|
Name string
|
|
Level int
|
|
Y float64
|
|
}
|
|
|
|
// pdfActionSource PDF动作来源
|
|
type pdfActionSource struct {
|
|
Box Box
|
|
Actions []Action
|
|
}
|
|
|
|
// newPDFNavigation 创建PDF导航信息
|
|
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据
|
|
// 返回: *pdfNavigation PDF导航信息
|
|
func newPDFNavigation(renderer *Renderer, doc *Document, pages []pdfPage) *pdfNavigation {
|
|
navigation := &pdfNavigation{
|
|
Anchor: make(map[int][]pdfAnchor),
|
|
Link: make(map[int][]pdfLink),
|
|
Outline: make(map[int][]pdfOutline),
|
|
}
|
|
pageIndex := make(map[string]int, len(pages))
|
|
for i, page := range pages {
|
|
pageIndex[page.Content.ID] = i
|
|
}
|
|
bookmarks := make(map[string]Dest)
|
|
if doc != nil {
|
|
for _, bookmark := range doc.Bookmarks.Bookmark {
|
|
bookmarks[bookmark.Name] = bookmark.Dest
|
|
}
|
|
}
|
|
for i, page := range pages {
|
|
sources := pageActionSources(page)
|
|
if renderer.RenderAnnotations {
|
|
sources = append(sources, annotationActionSources(renderer.Reader.Annots[page.Content.ID])...)
|
|
}
|
|
for _, source := range sources {
|
|
rect := pdfSourceRect(source.Box, page.Box.H)
|
|
for _, action := range source.Actions {
|
|
if action.Event != "CLICK" {
|
|
continue
|
|
}
|
|
navigation.addAction(i, rect, action, bookmarks, pageIndex, pages)
|
|
}
|
|
}
|
|
}
|
|
if doc != nil {
|
|
navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages)
|
|
}
|
|
return navigation
|
|
}
|
|
|
|
// addAction 添加PDF动作
|
|
// 入参: page 页面索引, rect 动作区域, action 动作, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
|
func (n *pdfNavigation) addAction(page int, rect canvas.Rect, action Action, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
|
|
if action.Goto != nil {
|
|
dest := gotoDest(action.Goto, bookmarks)
|
|
if dest == nil {
|
|
return
|
|
}
|
|
target, ok := pageIndex[dest.PageID]
|
|
if !ok {
|
|
return
|
|
}
|
|
targetRect, ok := pdfDestRect(*dest, pages[target].Box.H)
|
|
if !ok {
|
|
return
|
|
}
|
|
name := fmt.Sprintf("ofdgo-dest-%d", n.nextID)
|
|
n.nextID++
|
|
n.Anchor[target] = append(n.Anchor[target], pdfAnchor{Name: name, Rect: targetRect})
|
|
n.Link[page] = append(n.Link[page], pdfLink{URI: "#" + name, Rect: rect})
|
|
return
|
|
}
|
|
if action.URI != nil && action.URI.URI != "" {
|
|
n.Link[page] = append(n.Link[page], pdfLink{URI: resolveActionURI(*action.URI), Rect: rect})
|
|
}
|
|
}
|
|
|
|
// addOutlines 添加PDF大纲
|
|
// 入参: outlines 大纲节点, level 节点层级, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
|
func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks map[string]Dest, pageIndex map[string]int, pages []pdfPage) {
|
|
for _, outline := range outlines {
|
|
if dest := outlineDest(outline, bookmarks); dest != nil {
|
|
if page, ok := pageIndex[dest.PageID]; ok {
|
|
n.Outline[page] = append(n.Outline[page], pdfOutline{
|
|
Name: outline.Title,
|
|
Level: level,
|
|
Y: pdfDestY(*dest, pages[page].Box.H),
|
|
})
|
|
}
|
|
}
|
|
n.addOutlines(outline.OutlineElem, level+1, bookmarks, pageIndex, pages)
|
|
}
|
|
}
|
|
|
|
// apply 应用PDF导航信息
|
|
// 入参: renderer PDF渲染器, page 页面索引
|
|
func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) {
|
|
for _, anchor := range n.Anchor[page] {
|
|
renderer.AddAnchor(anchor.Name, anchor.Rect)
|
|
}
|
|
for _, link := range n.Link[page] {
|
|
renderer.AddLink(link.URI, link.Rect)
|
|
}
|
|
for _, outline := range n.Outline[page] {
|
|
renderer.AddOutline(outline.Name, outline.Level, outline.Y)
|
|
}
|
|
}
|
|
|
|
// pageActionSources 获取页面动作来源
|
|
// 入参: page 页面数据
|
|
// 返回: []pdfActionSource 动作来源
|
|
func pageActionSources(page pdfPage) []pdfActionSource {
|
|
sources := make([]pdfActionSource, 0)
|
|
if len(page.Content.Actions) > 0 {
|
|
sources = append(sources, pdfActionSource{
|
|
Box: Box{W: page.Box.W, H: page.Box.H},
|
|
Actions: page.Content.Actions,
|
|
})
|
|
}
|
|
for _, layer := range page.Content.Content.Layer {
|
|
for _, object := range layer.Objects {
|
|
sources = appendGraphicActionSources(sources, object, nil)
|
|
}
|
|
}
|
|
return sources
|
|
}
|
|
|
|
// annotationActionSources 获取注释动作来源
|
|
// 入参: annotations 页面注释
|
|
// 返回: []pdfActionSource 动作来源
|
|
func annotationActionSources(annotations []Annotation) []pdfActionSource {
|
|
sources := make([]pdfActionSource, 0)
|
|
for _, annotation := range annotations {
|
|
box, err := ParseBox(annotation.Appearance.Boundary)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, object := range annotation.Appearance.Objects {
|
|
sources = appendGraphicActionSources(sources, object, &box)
|
|
}
|
|
}
|
|
return sources
|
|
}
|
|
|
|
// appendGraphicActionSources 添加图形对象动作来源
|
|
// 入参: sources 动作来源, object 图形对象, box 指定动作区域
|
|
// 返回: []pdfActionSource 动作来源
|
|
func appendGraphicActionSources(sources []pdfActionSource, object GraphicObject, box *Box) []pdfActionSource {
|
|
var boundary string
|
|
var actions []Action
|
|
var children []GraphicObject
|
|
switch object.Type {
|
|
case "TextObject":
|
|
boundary = object.TextObject.Boundary
|
|
actions = object.TextObject.Actions
|
|
case "PathObject":
|
|
boundary = object.PathObject.Boundary
|
|
actions = object.PathObject.Actions
|
|
case "ImageObject":
|
|
boundary = object.ImageObject.Boundary
|
|
actions = object.ImageObject.Actions
|
|
case "CompositeGraphicUnit", "CompositeObject":
|
|
boundary = object.CompositeGraphicUnit.Boundary
|
|
actions = object.CompositeGraphicUnit.Actions
|
|
children = object.CompositeGraphicUnit.Objects
|
|
}
|
|
sourceBox := box
|
|
if sourceBox == nil && boundary != "" {
|
|
if value, err := ParseBox(boundary); err == nil {
|
|
sourceBox = &value
|
|
}
|
|
}
|
|
if sourceBox != nil && len(actions) > 0 {
|
|
sources = append(sources, pdfActionSource{Box: *sourceBox, Actions: actions})
|
|
}
|
|
for _, child := range children {
|
|
sources = appendGraphicActionSources(sources, child, box)
|
|
}
|
|
return sources
|
|
}
|
|
|
|
// gotoDest 获取文档内跳转目标
|
|
// 入参: action 跳转动作, bookmarks 书签
|
|
// 返回: *Dest 跳转目标
|
|
func gotoDest(action *Goto, bookmarks map[string]Dest) *Dest {
|
|
if action.Dest != nil {
|
|
return action.Dest
|
|
}
|
|
if action.Bookmark != nil {
|
|
if dest, ok := bookmarks[action.Bookmark.Name]; ok {
|
|
return &dest
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// outlineDest 获取大纲跳转目标
|
|
// 入参: outline 大纲节点, bookmarks 书签
|
|
// 返回: *Dest 跳转目标
|
|
func outlineDest(outline OutlineElem, bookmarks map[string]Dest) *Dest {
|
|
for _, action := range outline.Actions {
|
|
if action.Goto != nil {
|
|
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
|
return dest
|
|
}
|
|
}
|
|
}
|
|
for _, child := range outline.OutlineElem {
|
|
if dest := outlineDest(child, bookmarks); dest != nil {
|
|
return dest
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// resolveActionURI 解析URI动作地址
|
|
// 入参: action URI动作
|
|
// 返回: string URI地址
|
|
func resolveActionURI(action URI) string {
|
|
if action.Base == "" {
|
|
return action.URI
|
|
}
|
|
base, err := url.Parse(action.Base)
|
|
if err != nil {
|
|
return action.URI
|
|
}
|
|
target, err := url.Parse(action.URI)
|
|
if err != nil {
|
|
return action.URI
|
|
}
|
|
return base.ResolveReference(target).String()
|
|
}
|
|
|
|
// pdfSourceRect 转换PDF动作区域
|
|
// 入参: box OFD区域, pageH 页面高度
|
|
// 返回: canvas.Rect PDF区域
|
|
func pdfSourceRect(box Box, pageH float64) canvas.Rect {
|
|
return canvas.RectFromSize(box.X, pageH-box.Y-box.H, box.W, box.H)
|
|
}
|
|
|
|
// pdfDestRect 转换PDF跳转目标
|
|
// 入参: dest OFD跳转目标, pageH 页面高度
|
|
// 返回: canvas.Rect PDF目标区域, bool 是否支持
|
|
func pdfDestRect(dest Dest, pageH float64) (canvas.Rect, bool) {
|
|
switch dest.Type {
|
|
case "XYZ":
|
|
return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Top, X1: dest.Left, Y1: pageH - dest.Top}, true
|
|
case "Fit":
|
|
return canvas.Rect{}, true
|
|
case "FitH":
|
|
return canvas.Rect{Y0: pageH - dest.Top, Y1: pageH - dest.Top}, true
|
|
case "FitV":
|
|
return canvas.Rect{X0: dest.Left, X1: dest.Left}, true
|
|
case "FitR":
|
|
return canvas.Rect{X0: dest.Left, Y0: pageH - dest.Bottom, X1: dest.Right, Y1: pageH - dest.Top}, true
|
|
}
|
|
return canvas.Rect{}, false
|
|
}
|
|
|
|
// pdfDestY 获取PDF大纲目标位置
|
|
// 入参: dest OFD跳转目标, pageH 页面高度
|
|
// 返回: float64 PDF纵坐标
|
|
func pdfDestY(dest Dest, pageH float64) float64 {
|
|
switch dest.Type {
|
|
case "XYZ", "FitH", "FitR":
|
|
return pageH - dest.Top
|
|
}
|
|
return 0
|
|
}
|