mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-26 13:48:55 +08:00

* Use page indirect object for internal outlines * Use page indirect object in creator outline destinations * Adapt creator test case to test outline creation and retrieval
322 lines
8.9 KiB
Go
322 lines
8.9 KiB
Go
/*
|
|
* This file is subject to the terms and conditions defined in
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
*/
|
|
|
|
package model
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/unidoc/unipdf/v3/common"
|
|
"github.com/unidoc/unipdf/v3/core"
|
|
)
|
|
|
|
// OutlineDest represents the destination of an outline item.
|
|
// It holds the page and the position on the page an outline item points to.
|
|
type OutlineDest struct {
|
|
PageObj *core.PdfIndirectObject `json:"-"`
|
|
Page int64 `json:"page"`
|
|
Mode string `json:"mode"`
|
|
X float64 `json:"x"`
|
|
Y float64 `json:"y"`
|
|
Zoom float64 `json:"zoom"`
|
|
}
|
|
|
|
// NewOutlineDest returns a new outline destination which can be used
|
|
// with outline items.
|
|
func NewOutlineDest(page int64, x, y float64) OutlineDest {
|
|
return OutlineDest{
|
|
Page: page,
|
|
Mode: "XYZ",
|
|
X: x,
|
|
Y: y,
|
|
}
|
|
}
|
|
|
|
// newOutlineDestFromPdfObject creates a new outline destination from the
|
|
// specified PDF object.
|
|
func newOutlineDestFromPdfObject(o core.PdfObject, r *PdfReader) (*OutlineDest, error) {
|
|
// Validate input PDF object.
|
|
destArr, ok := core.GetArray(o)
|
|
if !ok {
|
|
return nil, errors.New("outline destination object must be an array")
|
|
}
|
|
|
|
destArrLen := destArr.Len()
|
|
if destArrLen < 2 {
|
|
return nil, fmt.Errorf("invalid outline destination array length: %d", destArrLen)
|
|
}
|
|
|
|
// Extract page number.
|
|
dest := &OutlineDest{Mode: "Fit"}
|
|
|
|
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 {
|
|
common.Log.Debug("WARN: could not get page index for page %+v", pageInd)
|
|
}
|
|
dest.PageObj = pageInd
|
|
} else if pageIdx, ok := core.GetIntVal(pageObj); ok {
|
|
// Page index is provided. Get indirect object to page.
|
|
if pageIdx >= 0 && pageIdx < len(r.PageList) {
|
|
dest.PageObj = r.PageList[pageIdx].GetPageAsIndirectObject()
|
|
} else {
|
|
common.Log.Debug("WARN: could not get page container for page %d", pageIdx)
|
|
}
|
|
dest.Page = int64(pageIdx)
|
|
} else {
|
|
return nil, fmt.Errorf("invalid outline destination page: %T", pageObj)
|
|
}
|
|
|
|
// Extract magnification mode.
|
|
mode, ok := core.GetNameVal(destArr.Get(1))
|
|
if !ok {
|
|
common.Log.Debug("invalid outline destination magnification mode: %v", destArr.Get(1))
|
|
return dest, nil
|
|
}
|
|
|
|
// Parse magnification mode parameters.
|
|
// See section 12.3.2.2 "Explicit Destinations" (page 374).
|
|
switch mode {
|
|
// [pageObj|pageNum /Fit]
|
|
// [pageObj|pageNum /FitB]
|
|
case "Fit", "FitB":
|
|
// [pageObj|pageNum /FitH top]
|
|
// [pageObj|pageNum /FitBH top]
|
|
case "FitH", "FitBH":
|
|
if destArrLen > 2 {
|
|
dest.Y, _ = core.GetNumberAsFloat(core.TraceToDirectObject(destArr.Get(2)))
|
|
}
|
|
// [pageObj|pageNum /FitV left]
|
|
// [pageObj|pageNum /FitBV left]
|
|
case "FitV", "FitBV":
|
|
if destArrLen > 2 {
|
|
dest.X, _ = core.GetNumberAsFloat(core.TraceToDirectObject(destArr.Get(2)))
|
|
}
|
|
// [pageObj|pageNum /XYZ x y zoom]
|
|
case "XYZ":
|
|
if destArrLen > 4 {
|
|
dest.X, _ = core.GetNumberAsFloat(core.TraceToDirectObject(destArr.Get(2)))
|
|
dest.Y, _ = core.GetNumberAsFloat(core.TraceToDirectObject(destArr.Get(3)))
|
|
dest.Zoom, _ = core.GetNumberAsFloat(core.TraceToDirectObject(destArr.Get(4)))
|
|
}
|
|
default:
|
|
mode = "Fit"
|
|
}
|
|
|
|
dest.Mode = mode
|
|
return dest, nil
|
|
}
|
|
|
|
// ToPdfObject returns a PDF object representation of the outline destination.
|
|
func (od OutlineDest) ToPdfObject() core.PdfObject {
|
|
if (od.PageObj == nil && od.Page < 0) || od.Mode == "" {
|
|
return core.MakeNull()
|
|
}
|
|
|
|
// Add destination page.
|
|
dest := core.MakeArray()
|
|
if od.PageObj != nil {
|
|
// Internal outline.
|
|
dest.Append(od.PageObj)
|
|
} else {
|
|
// External outline.
|
|
dest.Append(core.MakeInteger(od.Page))
|
|
}
|
|
|
|
// Add destination mode.
|
|
dest.Append(core.MakeName(od.Mode))
|
|
|
|
// See section 12.3.2.2 "Explicit Destinations" (page 374).
|
|
switch od.Mode {
|
|
// [pageObj|pageNum /Fit]
|
|
// [pageObj|pageNum /FitB]
|
|
case "Fit", "FitB":
|
|
// [pageObj|pageNum /FitH top]
|
|
// [pageObj|pageNum /FitBH top]
|
|
case "FitH", "FitBH":
|
|
dest.Append(core.MakeFloat(od.Y))
|
|
// [pageObj|pageNum /FitV left]
|
|
// [pageObj|pageNum /FitBV left]
|
|
case "FitV", "FitBV":
|
|
dest.Append(core.MakeFloat(od.X))
|
|
// [pageObj|pageNum /XYZ x y zoom]
|
|
case "XYZ":
|
|
dest.Append(core.MakeFloat(od.X))
|
|
dest.Append(core.MakeFloat(od.Y))
|
|
dest.Append(core.MakeFloat(od.Zoom))
|
|
default:
|
|
dest.Set(1, core.MakeName("Fit"))
|
|
}
|
|
|
|
return dest
|
|
}
|
|
|
|
// Outline represents a PDF outline dictionary (Table 152 - p. 376).
|
|
// Currently, the Outline object can only be used to construct PDF outlines.
|
|
type Outline struct {
|
|
Entries []*OutlineItem `json:"entries,omitempty"`
|
|
}
|
|
|
|
// NewOutline returns a new outline instance.
|
|
func NewOutline() *Outline {
|
|
return &Outline{}
|
|
}
|
|
|
|
// Add appends a top level outline item to the outline.
|
|
func (o *Outline) Add(item *OutlineItem) {
|
|
o.Entries = append(o.Entries, item)
|
|
}
|
|
|
|
// Insert adds a top level outline item in the outline,
|
|
// at the specified index.
|
|
func (o *Outline) Insert(index uint, item *OutlineItem) {
|
|
l := uint(len(o.Entries))
|
|
if index > l {
|
|
index = l
|
|
}
|
|
|
|
o.Entries = append(o.Entries[:index], append([]*OutlineItem{item}, o.Entries[index:]...)...)
|
|
}
|
|
|
|
// Items returns all children outline items.
|
|
func (o *Outline) Items() []*OutlineItem {
|
|
return o.Entries
|
|
}
|
|
|
|
// ToPdfOutline returns a low level PdfOutline object, based on the current
|
|
// instance.
|
|
func (o *Outline) ToPdfOutline() *PdfOutline {
|
|
// Create outline.
|
|
outline := NewPdfOutline()
|
|
|
|
// Create outline items.
|
|
var outlineItems []*PdfOutlineItem
|
|
var lenDescendants int64
|
|
var prev *PdfOutlineItem
|
|
|
|
for _, item := range o.Entries {
|
|
outlineItem, lenChildren := item.ToPdfOutlineItem()
|
|
outlineItem.Parent = &outline.PdfOutlineTreeNode
|
|
|
|
if prev != nil {
|
|
prev.Next = &outlineItem.PdfOutlineTreeNode
|
|
outlineItem.Prev = &prev.PdfOutlineTreeNode
|
|
}
|
|
|
|
outlineItems = append(outlineItems, outlineItem)
|
|
lenDescendants += lenChildren
|
|
prev = outlineItem
|
|
}
|
|
|
|
// Add outline linked list properties.
|
|
lenOutlineItems := int64(len(outlineItems))
|
|
lenDescendants += int64(lenOutlineItems)
|
|
|
|
if lenOutlineItems > 0 {
|
|
outline.First = &outlineItems[0].PdfOutlineTreeNode
|
|
outline.Last = &outlineItems[lenOutlineItems-1].PdfOutlineTreeNode
|
|
outline.Count = &lenDescendants
|
|
}
|
|
|
|
return outline
|
|
}
|
|
|
|
// ToOutlineTree returns a low level PdfOutlineTreeNode object, based on
|
|
// the current instance.
|
|
func (o *Outline) ToOutlineTree() *PdfOutlineTreeNode {
|
|
return &o.ToPdfOutline().PdfOutlineTreeNode
|
|
}
|
|
|
|
// ToPdfObject returns a PDF object representation of the outline.
|
|
func (o *Outline) ToPdfObject() core.PdfObject {
|
|
return o.ToPdfOutline().ToPdfObject()
|
|
}
|
|
|
|
// OutlineItem represents a PDF outline item dictionary (Table 153 - pp. 376 - 377).
|
|
type OutlineItem struct {
|
|
Title string `json:"title"`
|
|
Dest OutlineDest `json:"dest"`
|
|
Entries []*OutlineItem `json:"entries,omitempty"`
|
|
}
|
|
|
|
// NewOutlineItem returns a new outline item instance.
|
|
func NewOutlineItem(title string, dest OutlineDest) *OutlineItem {
|
|
return &OutlineItem{
|
|
Title: title,
|
|
Dest: dest,
|
|
}
|
|
}
|
|
|
|
// Add appends an outline item as a child of the current outline item.
|
|
func (oi *OutlineItem) Add(item *OutlineItem) {
|
|
oi.Entries = append(oi.Entries, item)
|
|
}
|
|
|
|
// Insert adds an outline item as a child of the current outline item,
|
|
// at the specified index.
|
|
func (oi *OutlineItem) Insert(index uint, item *OutlineItem) {
|
|
l := uint(len(oi.Entries))
|
|
if index > l {
|
|
index = l
|
|
}
|
|
|
|
oi.Entries = append(oi.Entries[:index], append([]*OutlineItem{item}, oi.Entries[index:]...)...)
|
|
}
|
|
|
|
// Items returns all children outline items.
|
|
func (oi *OutlineItem) Items() []*OutlineItem {
|
|
return oi.Entries
|
|
}
|
|
|
|
// ToPdfOutlineItem returns a low level PdfOutlineItem object,
|
|
// based on the current instance.
|
|
func (oi *OutlineItem) ToPdfOutlineItem() (*PdfOutlineItem, int64) {
|
|
// Create outline item.
|
|
currItem := NewPdfOutlineItem()
|
|
currItem.Title = core.MakeEncodedString(oi.Title, true)
|
|
currItem.Dest = oi.Dest.ToPdfObject()
|
|
|
|
// Create outline items.
|
|
var outlineItems []*PdfOutlineItem
|
|
var lenDescendants int64
|
|
var prev *PdfOutlineItem
|
|
|
|
for _, item := range oi.Entries {
|
|
outlineItem, lenChildren := item.ToPdfOutlineItem()
|
|
outlineItem.Parent = &currItem.PdfOutlineTreeNode
|
|
|
|
if prev != nil {
|
|
prev.Next = &outlineItem.PdfOutlineTreeNode
|
|
outlineItem.Prev = &prev.PdfOutlineTreeNode
|
|
}
|
|
|
|
outlineItems = append(outlineItems, outlineItem)
|
|
lenDescendants += lenChildren
|
|
prev = outlineItem
|
|
}
|
|
|
|
// Add outline item linked list properties.
|
|
lenOutlineItems := len(outlineItems)
|
|
lenDescendants += int64(lenOutlineItems)
|
|
|
|
if lenOutlineItems > 0 {
|
|
currItem.First = &outlineItems[0].PdfOutlineTreeNode
|
|
currItem.Last = &outlineItems[lenOutlineItems-1].PdfOutlineTreeNode
|
|
currItem.Count = &lenDescendants
|
|
}
|
|
|
|
return currItem, lenDescendants
|
|
}
|
|
|
|
// ToPdfObject returns a PDF object representation of the outline item.
|
|
func (oi *OutlineItem) ToPdfObject() core.PdfObject {
|
|
outlineItem, _ := oi.ToPdfOutlineItem()
|
|
return outlineItem.ToPdfObject()
|
|
}
|