// 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() }