diff --git a/model/outline.go b/model/outline.go index 9f2fa122..b813ec09 100644 --- a/model/outline.go +++ b/model/outline.go @@ -6,6 +6,9 @@ package model import ( + "errors" + + "github.com/unidoc/unipdf/v3/common" "github.com/unidoc/unipdf/v3/core" ) @@ -27,6 +30,53 @@ func NewOutlineDest(page int64, x, y float64) OutlineDest { } } +// newOutlineDestFromPdfObject creates a new outline destination from the +// specified PDF object. +func newOutlineDestFromPdfObject(o core.PdfObject, r *PdfReader) OutlineDest { + dest := OutlineDest{} + + destArr, ok := core.GetArray(o) + if !ok { + return dest + } + + // Covered destination formats: + // [pageObj|pageNum /Fit] + // [pageObj|pageNum /FitB] + // [pageObj|pageNum /FitH top] + // [pageObj|pageNum /FitV left] + // [pageObj|pageNum /FitBH top] + // [pageObj|pageNum /XYZ x y zoom] + // [pageObj|pageNum /FitR left bottom right top] + // See section 12.3.2.2 "Explicit Destinations" (page 374). + destArrLen := destArr.Len() + if destArrLen < 2 { + return dest + } + + // Extract page number. + pageObj := destArr.Get(0) + if pageInd, ok := core.GetIndirect(pageObj); ok { + // Page object is provided. Identify page number using the reader. + if _, pageNum, err := r.PageFromIndirectObject(pageInd); err == nil { + dest.Page = int64(pageNum - 1) + } + } else if pageNum, ok := core.GetIntVal(pageObj); ok { + // Page number is provided. + dest.Page = int64(pageNum) + } + + // Extract destination coordinates. + if destArrLen == 5 { + if xyz, ok := core.GetName(destArr.Get(1)); ok && xyz.String() == "XYZ" { + dest.X, _ = core.GetFloatVal(destArr.Get(2)) + dest.Y, _ = core.GetFloatVal(destArr.Get(3)) + } + } + + return dest +} + // ToPdfObject returns a PDF object representation of the outline destination. func (od OutlineDest) ToPdfObject() core.PdfObject { return core.MakeArray( @@ -49,7 +99,54 @@ func NewOutline() *Outline { return &Outline{} } -func NewOutlineFromOutlineTree(node *PdfOutlineTreeNode) { +// NewOutlineFromReaderOutline returns a new outline from the outline tree of +// the passed in reader. +func NewOutlineFromReaderOutline(r *PdfReader) (*Outline, error) { + if r == nil { + return nil, errors.New("cannot create outline from nil reader") + } + + outlineTree := r.GetOutlineTree() + if outlineTree == nil { + return nil, errors.New("the specified reader does not have an outline tree") + } + + var traverseFunc func(node *PdfOutlineTreeNode, entries *[]*OutlineItem) + traverseFunc = func(node *PdfOutlineTreeNode, entries *[]*OutlineItem) { + if node == nil { + return + } + if node.context == nil { + common.Log.Debug("ERROR: missing outline entry context") + return + } + + // Check if node is an outline item. + var entry *OutlineItem + if item, ok := node.context.(*PdfOutlineItem); ok { + entry = NewOutlineItem(item.Title.Decoded(), newOutlineDestFromPdfObject(item.Dest, r)) + *entries = append(*entries, entry) + + // Traverse next node. + if item.Next != nil { + traverseFunc(item.Next, entries) + } + } + + // Check if node has children. + if node.First != nil { + if entry != nil { + entries = &entry.Entries + } + + // Traverse node children. + traverseFunc(node.First, entries) + } + } + + outline := NewOutline() + traverseFunc(outlineTree, &outline.Entries) + return outline, nil } // Add appends a top level outline item to the outline.