From 65a839f1e8c625b7357a869909e3c4b8426cb29a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 9 Oct 2026 20:45:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=9B=AE=E5=BD=95=E5=AF=BC=E5=85=A5=EF=BC=8C?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=AF=BC=E8=88=AA=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_pdf.go | 3 ++ ofdgo_pdf_link.go | 6 +++ ofdgo_pdf_outline.go | 122 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 ofdgo_pdf_outline.go diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 451ff87..b8a76ea 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -215,6 +215,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI } if len(editor.pages) != 0 { importer.page = -1 + if err := importer.outlines(ctx); err != nil { + return nil, PDFImportReport{}, fmt.Errorf("import PDF outlines: %w", err) + } if err := importer.documentOpenActions(ctx); err != nil { return nil, PDFImportReport{}, fmt.Errorf("import PDF document actions: %w", err) } diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index 58e3e8b..5757973 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -406,8 +406,14 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre case pdfgo.Name("LastPage"): index = len(p.editor.pages) - 1 case pdfgo.Name("NextPage"): + if index < 0 { + return nil, &pdfgo.UnsupportedError{Feature: "relative page action without a fixed current page"} + } index = min(index+1, len(p.editor.pages)-1) case pdfgo.Name("PrevPage"): + if index < 0 { + return nil, &pdfgo.UnsupportedError{Feature: "relative page action without a fixed current page"} + } index = max(index-1, 0) default: return nil, nil diff --git a/ofdgo_pdf_outline.go b/ofdgo_pdf_outline.go new file mode 100644 index 0000000..f6eb96c --- /dev/null +++ b/ofdgo_pdf_outline.go @@ -0,0 +1,122 @@ +// 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 ( + "bytes" + "context" + "encoding/xml" + "errors" + "fmt" + "strconv" + + "github.com/xiaoqidun/pdfgo" +) + +// outlines 按先序批量转换目录,保留层级、展开状态和动作顺序 +// 入参: ctx 取消上下文 +// 返回: error 严格模式转换错误或取消错误 +func (p *pdfImporter) outlines(ctx context.Context) error { + count := 0 + data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { + x.root("Outlines", nil) + depth := 0 + err := p.reader.WalkOutlines(ctx, func(level int, item pdfgo.OutlineItem) error { + actions, err := p.outlineActions(ctx, item) + if err != nil { + if p.warning == nil || ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline %q actions not transferred to OFD: %v", item.Title, err)}) + actions = nil + } + for depth > level { + x.end("OutlineElem") + depth-- + } + attrs := ofdAttrs{{Name: xml.Name{Local: "Title"}, Value: item.Title}} + attrs.add("Expanded", strconv.FormatBool(item.Expanded)) + x.start("OutlineElem", attrs) + x.actions(actions) + depth++ + count++ + return x.err + }) + if err != nil { + x.err = err + return + } + for depth > 0 { + x.end("OutlineElem") + depth-- + } + x.end("Outlines") + }) + if err != nil { + if ctx.Err() != nil { + return ctx.Err() + } + if p.warning == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } + p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outlines not transferred to OFD: %v", err)}) + return nil + } + if count > 0 { + p.editor.outlines = bytes.TrimPrefix(data, []byte(xml.Header)) + } + return nil +} + +// outlineActions 转换目录目标及动作链,不为无目标节点补充跳转 +// 入参: ctx 取消上下文, item PDF目录节点 +// 返回: []Action OFD动作序列, error 动作或取消错误 +func (p *pdfImporter) outlineActions(ctx context.Context, item pdfgo.OutlineItem) ([]Action, error) { + object, err := p.reader.Resolve(item.Action) + if err != nil { + return nil, err + } + dest, err := p.reader.Resolve(item.Destination) + if err != nil { + return nil, err + } + if dest != nil { + if object != nil { + return nil, fmt.Errorf("PDF outline has both destination and action") + } + object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": dest} + } + currentPage := -1 + var actions []Action + err = p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error { + action, err := p.linkAction(pdfgo.Annotation{Dictionary: pdfgo.Dictionary{"A": info.Dictionary}}, p.warning == nil, ¤tPage) + if err != nil { + var unsupported *pdfgo.UnsupportedError + if p.warning != nil && (errors.As(err, &unsupported) || errors.Is(err, pdfgo.ErrInvalidDestination) || errors.Is(err, pdfgo.ErrInvalidAction)) { + p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline action not transferred to OFD: %v", err)}) + currentPage = -1 + return nil + } + return err + } + if action != nil { + actions = append(actions, *action) + } else { + currentPage = -1 + } + return nil + }) + return actions, err +}