Merge pull request #233 from adrg/improve-outline-dest-parsing

Improve outline destination parsing
This commit is contained in:
Gunnsteinn Hall 2020-01-21 20:44:07 +00:00 committed by GitHub
commit 843a48ed7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 104 additions and 35 deletions

View File

@ -6,6 +6,10 @@
package model
import (
"errors"
"fmt"
"github.com/unidoc/unipdf/v3/common"
"github.com/unidoc/unipdf/v3/core"
)
@ -13,8 +17,10 @@ import (
// It holds the page and the position on the page an outline item points to.
type OutlineDest struct {
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
@ -22,6 +28,7 @@ type OutlineDest struct {
func NewOutlineDest(page int64, x, y float64) OutlineDest {
return OutlineDest{
Page: page,
Mode: "XYZ",
X: x,
Y: y,
}
@ -29,29 +36,21 @@ 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{}
func newOutlineDestFromPdfObject(o core.PdfObject, r *PdfReader) (*OutlineDest, error) {
// Validate input PDF object.
destArr, ok := core.GetArray(o)
if !ok {
return dest
return nil, errors.New("outline destination object must be an array")
}
// 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
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.
@ -61,34 +60,90 @@ func newOutlineDestFromPdfObject(o core.PdfObject, r *PdfReader) OutlineDest {
} else if pageNum, ok := core.GetIntVal(pageObj); ok {
// Page number is provided.
dest.Page = int64(pageNum)
} else {
return nil, fmt.Errorf("invalid outline destination page: %T", pageObj)
}
// Extract destination coordinates.
if destArrLen == 5 {
if xyz, ok := core.GetName(destArr.Get(1)); ok && xyz.String() == "XYZ" {
// 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.Page < 0 || od.Mode == "" {
return core.MakeNull()
}
dest := core.MakeArray(
core.MakeInteger(od.Page),
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
}
// ToPdfObject returns a PDF object representation of the outline destination.
func (od OutlineDest) ToPdfObject() core.PdfObject {
return core.MakeArray(
core.MakeInteger(od.Page),
core.MakeName("XYZ"),
core.MakeFloat(od.X),
core.MakeFloat(od.Y),
core.MakeFloat(0),
)
}
// 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"`
Entries []*OutlineItem `json:"entries,omitempty"`
}
// NewOutline returns a new outline instance.
@ -166,7 +221,7 @@ func (o *Outline) ToPdfObject() core.PdfObject {
type OutlineItem struct {
Title string `json:"title"`
Dest OutlineDest `json:"dest"`
Entries []*OutlineItem `json:"entries"`
Entries []*OutlineItem `json:"entries,omitempty"`
}
// NewOutlineItem returns a new outline item instance.

View File

@ -69,8 +69,5 @@ func TestGetOutlines(t *testing.T) {
require.NoError(t, err)
dstJson, err := json.Marshal(dstOutline)
require.NoError(t, err)
t.Log(string(srcJson))
t.Log(string(dstJson))
require.Equal(t, srcJson, dstJson)
}

View File

@ -463,8 +463,25 @@ func (r *PdfReader) GetOutlines() (*Outline, error) {
// 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))
// Search for outline destination object.
destObj := item.Dest
if (destObj == nil || core.IsNullObject(destObj)) && item.A != nil {
if actionDict, ok := core.GetDict(item.A); ok {
destObj, _ = core.GetArray(actionDict.Get("D"))
}
}
// Parse outline destination object.
var dest OutlineDest
if destObj != nil && !core.IsNullObject(destObj) {
if d, err := newOutlineDestFromPdfObject(destObj, r); err == nil {
dest = *d
} else {
common.Log.Debug("WARN: could not parse outline dest (%v): %v", destObj, err)
}
}
entry = NewOutlineItem(item.Title.Decoded(), dest)
*entries = append(*entries, entry)
// Traverse next node.