From 9b38f6f03967d8e4858d991950fe5fb0277ed75a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 9 Oct 2026 21:12:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E9=93=BE=E6=8E=A5=E5=8C=BA=E5=9F=9F=E4=B8=8E?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- ofdgo_renderer_pdf_action.go | 215 +++++++++++++++++++++++++++++++++++ ofdgo_renderer_pdf_canvas.go | 118 ++++++------------- 4 files changed, 253 insertions(+), 86 deletions(-) create mode 100644 ofdgo_renderer_pdf_action.go diff --git a/go.mod b/go.mod index 9a2bf0a..8742344 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893 github.com/xiaoqidun/jbig2 v1.1.3 - github.com/xiaoqidun/pdfgo v0.0.0-20261009121346-abd17a2aa440 + github.com/xiaoqidun/pdfgo v0.0.0-20261009130638-37c73506b373 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 0227e41..f197c22 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893 h1:jmQlBO7R+bcimOM github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/jbig2 v1.1.3 h1:p0d8FMLiLfzQuX278T4VTfTz8tIJEw87u4v63lH4CBc= github.com/xiaoqidun/jbig2 v1.1.3/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20261009121346-abd17a2aa440 h1:auHwW058jOQGHVB6FXNfT6kdLh5DU6KByu4DGDSLJ7U= -github.com/xiaoqidun/pdfgo v0.0.0-20261009121346-abd17a2aa440/go.mod h1:swzoqA5+wSHsRPesJS1VOMLhSyWkLCSQ+IgHNYXbcKo= +github.com/xiaoqidun/pdfgo v0.0.0-20261009130638-37c73506b373 h1:jw9W5q/sUnMOSEbYQD5U6Ppd0cDHXTuzREOwXm6ABtk= +github.com/xiaoqidun/pdfgo v0.0.0-20261009130638-37c73506b373/go.mod h1:swzoqA5+wSHsRPesJS1VOMLhSyWkLCSQ+IgHNYXbcKo= 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/ofdgo_renderer_pdf_action.go b/ofdgo_renderer_pdf_action.go new file mode 100644 index 0000000..3169ef6 --- /dev/null +++ b/ofdgo_renderer_pdf_action.go @@ -0,0 +1,215 @@ +// 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" + "slices" + + "github.com/tdewolff/canvas" + "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 + } + value, err := pdfNavigationTarget(action, bookmarks, pageIndex, pages) + if err != nil { + return err + } + if value == nil { + continue + } + box, outline, err := renderer.actionLinkRegion(source, action) + if err != nil { + return err + } + if box.W <= 0 || box.H <= 0 { + continue + } + path := geometryRectangle(box) + if outline != "" { + parsed, err := canvas.ParseSVGPath(outline) + if err != nil { + return err + } + if parsed.Len() > pdfLinkEdgeLimit { + return fmt.Errorf("PDF link region exceeds edge limit") + } + parsed, err = flattenPDFLink(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 := pdfActionRegion(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.exactLinks = 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) > pdfLinkEdgeLimit { + 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) > pdfLinkEdgeLimit { + return nil, fmt.Errorf("PDF action partition exceeds region limit") + } + edges := 0 + for _, area := range result { + edges += len(area.Path) + if edges > pdfLinkQuadLimit { + 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) > pdfLinkEdgeLimit || len(right) > pdfLinkEdgeLimit { + 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) > pdfLinkEdgeLimit { + 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 { + value := pdfgo.NavigationAction{URI: action.URI} + if action.Target != nil { + dest := action.Target.Destination + dest.Page = pages[action.Target.Page] + value.Destination = &dest + } + values = append(values, value) + } + return values +} diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index a1c44b3..5bb6120 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -46,10 +46,9 @@ type pdfNavigation struct { // pdfLink PDF链接 type pdfLink struct { - URI string - Rect canvas.Rect - Region *pdfgo.LinkRegion - Target *pdfTarget + Rect canvas.Rect + Region *pdfgo.LinkRegion + Actions []pdfNavigationAction } // pdfTarget 保存输出页索引及默认用户空间中的跳转参数 @@ -62,11 +61,11 @@ type pdfTarget struct { type pdfOutline struct { Name string Level int - Actions []pdfOutlineAction + Actions []pdfNavigationAction } -// pdfOutlineAction 保存目录的导航动作,输出时关联实际页面引用 -type pdfOutlineAction struct { +// pdfNavigationAction 保存链接或目录的导航动作,输出时关联实际页面引用 +type pdfNavigationAction struct { URI string Target *pdfTarget } @@ -154,37 +153,8 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP sources = append(sources, renderer.annotationActionSources(renderer.Reader.Annots[page.Content.ID])...) } for _, source := range sources { - for _, action := range source.Actions { - if action.Event != "CLICK" { - continue - } - box, outline, err := renderer.actionLinkRegion(source, action) - if err != nil { - return nil, err - } - if box.W <= 0 || box.H <= 0 { - continue - } - rect := pdfSourceRect(box, page.Box.H) - count := len(navigation.Link[i]) - if err := navigation.addAction(i, rect, action, bookmarks, pageIndex, pages); err != nil { - return nil, err - } - if len(navigation.Link[i]) == count || outline == "" { - continue - } - region, err := pdfActionRegion(renderer.outputContext(), outline, page.Box.H) - if err != nil { - return nil, err - } - if region != nil && len(region.Quads) == 0 { - navigation.Link[i] = navigation.Link[i][:count] - continue - } - if region != nil { - navigation.Link[i][count].Region = region - navigation.exactLinks = true - } + if err := navigation.addActions(renderer, i, source, bookmarks, pageIndex, pages); err != nil { + return nil, err } } } @@ -196,31 +166,29 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP return navigation, nil } -// addAction 添加PDF动作 -// 入参: page 页面索引, rect 动作区域, action 动作, bookmarks 书签, pageIndex 页面索引表, pages 页面数据 -// 返回: error 目标参数错误 -func (n *pdfNavigation) addAction(page int, rect canvas.Rect, action Action, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) error { +// pdfNavigationTarget 转换导航动作,选页未包含的目标不写入 +// 入参: action 动作, bookmarks 书签, pageIndex 页面索引表, pages 页面数据 +// 返回: *pdfNavigationAction 导航动作,nil表示无输出动作, error 目标参数错误 +func pdfNavigationTarget(action Action, bookmarks map[string]Dest, pageIndex map[string]int, pages []RenderDocumentPage) (*pdfNavigationAction, error) { if action.Goto != nil { dest := gotoDest(action.Goto, bookmarks) if dest == nil { - return nil + return nil, nil } target, ok := pageIndex[dest.PageID] if !ok { - return nil + return nil, nil } destination, err := pdfDestination(*dest, pages[target].Box.H) if err != nil { - return err + return nil, err } - n.Link[page] = append(n.Link[page], pdfLink{Rect: rect, Target: &pdfTarget{Page: target, Destination: destination}}) - n.exactLinks = true - return nil + return &pdfNavigationAction{Target: &pdfTarget{Page: target, Destination: destination}}, nil } if action.URI != nil && action.URI.URI != "" { - n.Link[page] = append(n.Link[page], pdfLink{URI: resolveActionURI(*action.URI), Rect: rect}) + return &pdfNavigationAction{URI: resolveActionURI(*action.URI)}, nil } - return nil + return nil, nil } // addOutlines 保留目录层级及节点自身的导航动作,选页时移除无保留子项的失效跳转节点 @@ -231,25 +199,15 @@ func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks entry := pdfOutline{Name: outline.Title, Level: level} onlyMissingTargets := len(outline.Actions) != 0 for _, action := range outline.Actions { - if action.Goto != nil { - dest := gotoDest(action.Goto, bookmarks) - if dest == nil { - continue - } - page, ok := pageIndex[dest.PageID] - if !ok { - continue - } - destination, err := pdfDestination(*dest, pages[page].Box.H) - if err != nil { - return err - } - entry.Actions = append(entry.Actions, pdfOutlineAction{Target: &pdfTarget{Page: page, Destination: destination}}) - } else { + if action.Goto == nil { onlyMissingTargets = false - if action.URI != nil && action.URI.URI != "" { - entry.Actions = append(entry.Actions, pdfOutlineAction{URI: resolveActionURI(*action.URI)}) - } + } + value, err := pdfNavigationTarget(action, bookmarks, pageIndex, pages) + if err != nil { + return err + } + if value != nil { + entry.Actions = append(entry.Actions, *value) } } index := len(n.Outline) @@ -269,7 +227,7 @@ func (n *pdfNavigation) addOutlines(outlines []OutlineElem, level int, bookmarks // 入参: renderer PDF渲染器, page 页面索引 func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) { for _, link := range n.Link[page] { - renderer.AddLink(link.URI, link.Rect) + renderer.AddLink("", link.Rect) } if page == 0 { for index, outline := range n.Outline { @@ -283,15 +241,19 @@ func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) { // 返回: error 目录结构或取消错误 func (n *pdfNavigation) destinations(ctx context.Context, reader *pdfgo.Reader, pages []pdfgo.Reference, options *pdfgo.RewriteOptions) error { options.LinkDestinations = make(map[pdfgo.AnnotationLocation]pdfgo.Destination) + options.LinkActions = make(map[pdfgo.AnnotationLocation][]pdfgo.NavigationAction) for page, links := range n.Link { for index, link := range links { if err := ctx.Err(); err != nil { return err } - if link.Target != nil { - dest := link.Target.Destination - dest.Page = pages[link.Target.Page] + if len(link.Actions) == 1 && link.Actions[0].Target != nil { + target := link.Actions[0].Target + dest := target.Destination + dest.Page = pages[target.Page] options.LinkDestinations[pdfgo.AnnotationLocation{Page: pages[page], Index: index}] = dest + } else { + options.LinkActions[pdfgo.AnnotationLocation{Page: pages[page], Index: index}] = pdfNavigationValues(link.Actions, pages) } } } @@ -313,17 +275,7 @@ func (n *pdfNavigation) destinations(ctx context.Context, reader *pdfgo.Reader, dest.Page = pages[target.Page] options.OutlineDestinations[item.Reference] = dest } else { - converted := make([]pdfgo.NavigationAction, 0, len(actions)) - for _, action := range actions { - value := pdfgo.NavigationAction{URI: action.URI} - if action.Target != nil { - dest := action.Target.Destination - dest.Page = pages[action.Target.Page] - value.Destination = &dest - } - converted = append(converted, value) - } - options.OutlineActions[item.Reference] = converted + options.OutlineActions[item.Reference] = pdfNavigationValues(actions, pages) } options.OutlineTitles[item.Reference] = n.Outline[count].Name count++