mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
286 lines
9.1 KiB
Go
286 lines
9.1 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 (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/tdewolff/canvas"
|
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
"github.com/xiaoqidun/pdfgo"
|
|
)
|
|
|
|
// pdfActionArea 保存互不重叠的点击区域及其有序动作
|
|
type pdfActionArea struct {
|
|
Path GeometryPath
|
|
Actions []pdfNavigationAction
|
|
}
|
|
|
|
// addActions 按命中区域组合同一对象的点击动作,不合并其他对象的事件
|
|
// 入参: renderer 渲染器, page 页面索引, source 动作来源, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
|
// 返回: error 区域、目标或取消错误
|
|
func (n *pdfNavigation) addActions(renderer *Renderer, page int, source actionSource, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) error {
|
|
ctx := renderer.outputContext()
|
|
var areas []pdfActionArea
|
|
for _, action := range source.Actions {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if action.Event != "CLICK" {
|
|
continue
|
|
}
|
|
box, outline, err := renderer.actionLinkRegion(source, action)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if box.W <= 0 || box.H <= 0 {
|
|
continue
|
|
}
|
|
value, err := n.actionTarget(action, page, source, bookmarks, pageIndex, pages)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if value == nil {
|
|
continue
|
|
}
|
|
path := GeometryPath(basegeom.Rectangle(box))
|
|
if outline != "" {
|
|
parsed, err := canvas.ParseSVGPath(outline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if parsed.Len() > codecpdf.LinkEdgeLimit {
|
|
return fmt.Errorf("PDF link region exceeds edge limit")
|
|
}
|
|
parsed, err = codecpdf.FlattenLink(ctx, parsed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path = *geometryFromCanvasPath(parsed.Transform(canvas.Identity.Scale(1, -1)))
|
|
}
|
|
areas, err = partitionPDFActions(ctx, renderer, areas, path, *value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, area := range areas {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
box, err := area.Path.Bounds()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if box.W <= 0 || box.H <= 0 {
|
|
continue
|
|
}
|
|
outline, err := area.Path.SVG()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
region, err := codecpdf.ActionRegion(ctx, outline, pages[page].Box.H)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if region != nil && len(region.Quads) == 0 {
|
|
continue
|
|
}
|
|
n.Link[page] = append(n.Link[page], pdfLink{Rect: pdfSourceRect(box, pages[page].Box.H), Region: region, Actions: area.Actions})
|
|
n.nativeWrite = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addLifecycleActions 按事件归属收集打开动作,不受点击区域限制
|
|
// 入参: ctx 取消上下文, source 动作来源, page 页面索引,负值表示文档, bookmarks 书签, pageIndex 页面索引表, pages 页面数据
|
|
// 返回: error 目标或取消错误
|
|
func (n *pdfNavigation) addLifecycleActions(ctx context.Context, source actionSource, page int, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) error {
|
|
event := "PO"
|
|
if page < 0 {
|
|
event = "DO"
|
|
}
|
|
for _, action := range source.Actions {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if action.Event != event || source.Graphic {
|
|
continue
|
|
}
|
|
value, err := n.actionTarget(action, page, source, bookmarks, pageIndex, pages)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if value == nil {
|
|
continue
|
|
}
|
|
if action.Event == "DO" {
|
|
n.Open = append(n.Open, *value)
|
|
} else {
|
|
n.PageOpen[page] = append(n.PageOpen[page], *value)
|
|
}
|
|
n.nativeWrite = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// partitionPDFActions 将新增动作区域与已有区域分离,使每个区域具有唯一动作序列
|
|
// 入参: ctx 取消上下文, renderer 渲染器, areas 已有分区, path 新动作区域, action 新动作
|
|
// 返回: []pdfActionArea 新分区, error 几何、复杂度或取消错误
|
|
func partitionPDFActions(ctx context.Context, renderer *Renderer, areas []pdfActionArea, path GeometryPath, action pdfNavigationAction) ([]pdfActionArea, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(areas) == 1 && slices.Equal(areas[0].Path, path) {
|
|
areas[0].Actions = append(areas[0].Actions, action)
|
|
return areas, nil
|
|
}
|
|
remaining := path
|
|
result := make([]pdfActionArea, 0, len(areas)+1)
|
|
box, err := path.Bounds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, area := range areas {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
other, err := area.Path.Bounds()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if box.X >= other.X+other.W || other.X >= box.X+box.W || box.Y >= other.Y+other.H || other.Y >= box.Y+box.H {
|
|
result = append(result, area)
|
|
continue
|
|
}
|
|
geometry, err := renderer.Geometry()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
common, err := combinePDFActionRegion(ctx, geometry, area.Path, path, GeometryIntersect)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(common) == 0 {
|
|
result = append(result, area)
|
|
continue
|
|
}
|
|
only, err := combinePDFActionRegion(ctx, geometry, area.Path, path, GeometrySubtract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(only) != 0 {
|
|
result = append(result, pdfActionArea{Path: only, Actions: area.Actions})
|
|
}
|
|
result = append(result, pdfActionArea{Path: common, Actions: append(slices.Clone(area.Actions), action)})
|
|
remaining, err = combinePDFActionRegion(ctx, geometry, remaining, area.Path, GeometrySubtract)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(result) > codecpdf.LinkEdgeLimit {
|
|
return nil, fmt.Errorf("PDF action partition exceeds region limit")
|
|
}
|
|
}
|
|
if len(remaining) != 0 {
|
|
result = append(result, pdfActionArea{Path: remaining, Actions: []pdfNavigationAction{action}})
|
|
}
|
|
if len(result) > codecpdf.LinkEdgeLimit {
|
|
return nil, fmt.Errorf("PDF action partition exceeds region limit")
|
|
}
|
|
edges := 0
|
|
for _, area := range result {
|
|
edges += len(area.Path)
|
|
if edges > codecpdf.LinkQuadLimit {
|
|
return nil, fmt.Errorf("PDF action partition exceeds total edge limit")
|
|
}
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// combinePDFActionRegion 对有界点击路径执行布尔运算,检查取消及中间结果规模
|
|
// 入参: ctx 取消上下文, geometry 几何后端, left 左路径, right 右路径, operation 运算类型
|
|
// 返回: GeometryPath 结果路径, error 几何、复杂度或取消错误
|
|
func combinePDFActionRegion(ctx context.Context, geometry GeometryBackend, left, right GeometryPath, operation GeometryOperation) (GeometryPath, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(left) > codecpdf.LinkEdgeLimit || len(right) > codecpdf.LinkEdgeLimit {
|
|
return nil, fmt.Errorf("PDF action partition exceeds edge limit")
|
|
}
|
|
result, err := geometry.Combine(left, right, operation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(result) > codecpdf.LinkEdgeLimit {
|
|
return nil, fmt.Errorf("PDF action partition exceeds edge limit")
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// pdfNavigationValues 将输出页索引替换为实际PDF页面引用
|
|
// 入参: actions 有序导航动作, pages 输出页面引用
|
|
// 返回: []pdfgo.NavigationAction 原生导航动作
|
|
func pdfNavigationValues(actions []pdfNavigationAction, pages []pdfgo.Reference) []pdfgo.NavigationAction {
|
|
values := make([]pdfgo.NavigationAction, 0, len(actions))
|
|
for _, action := range actions {
|
|
if action.Movie != nil {
|
|
values = append(values, action.Movie.values()...)
|
|
continue
|
|
}
|
|
value := pdfgo.NavigationAction{URI: action.URI, Attachment: action.Attachment, Sound: action.Sound}
|
|
if action.Target != nil {
|
|
dest := action.Target.Destination
|
|
dest.Page = pages[action.Target.Page]
|
|
value.Destination = &dest
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
return values
|
|
}
|
|
|
|
// writeAttachments 在页面绘制后读取附件,保留文件字节,不自动添加个人信息和时间
|
|
// 入参: ctx 取消上下文, options PDF写出配置
|
|
// 返回: error 附件读取或取消错误
|
|
func (n *pdfNavigation) writeAttachments(ctx context.Context, options *pdfgo.RewriteOptions) error {
|
|
if len(n.attachments) == 0 {
|
|
return nil
|
|
}
|
|
options.Attachments = make(map[string]*pdfgo.EmbeddedFile, len(n.attachments))
|
|
for _, id := range slices.Sorted(maps.Keys(n.attachments)) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
attachment := n.attachments[id]
|
|
input, err := n.source.openFile(n.source.ResPath(attachment.FileLoc))
|
|
if err != nil {
|
|
return fmt.Errorf("read attachment %s: %w", id, err)
|
|
}
|
|
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
|
|
closeErr := input.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("read attachment %s: %w", id, err)
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
options.Attachments[id] = &pdfgo.EmbeddedFile{Name: attachment.FileName(), Data: data}
|
|
}
|
|
return ctx.Err()
|
|
}
|