Merge pull request #321 from adrg/pdf-outlines

Add Creator support for outlines
This commit is contained in:
Gunnsteinn Hall 2019-01-18 14:33:20 +00:00 committed by GitHub
commit 33d17ce9aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 358 additions and 49 deletions

View File

@ -11,6 +11,7 @@ import (
"strconv"
"github.com/unidoc/unidoc/common"
"github.com/unidoc/unidoc/pdf/model"
)
// Chapter is used to arrange multiple drawables (paragraphs, images, etc) into a single section.
@ -39,12 +40,18 @@ type Chapter struct {
// Margins to be applied around the block when drawing on Page.
margins margins
// Reference to the creator's TOC.
// Reference to the TOC of the creator.
toc *TOC
// Reference to the outline of the creator.
outline *model.Outline
// The item of the chapter in the outline.
outlineItem *model.OutlineItem
}
// newChapter creates a new chapter with the specified title as the heading.
func newChapter(toc *TOC, title string, number int, style TextStyle) *Chapter {
func newChapter(toc *TOC, outline *model.Outline, title string, number int, style TextStyle) *Chapter {
p := newParagraph(fmt.Sprintf("%d. %s", number, title), style)
p.SetFont(style.Font)
p.SetFontSize(16)
@ -55,6 +62,7 @@ func newChapter(toc *TOC, title string, number int, style TextStyle) *Chapter {
showNumbering: true,
includeInTOC: true,
toc: toc,
outline: outline,
heading: p,
contents: []Drawable{},
}
@ -141,21 +149,43 @@ func (chap *Chapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
ctx.Page++ // Did not fit, moved to new Page block.
}
if chap.includeInTOC {
// Add to TOC.
chapNumber := ""
if chap.showNumbering {
if chap.number != 0 {
chapNumber = strconv.Itoa(chap.number) + "."
}
}
// Generate chapter title and number.
chapTitle := chap.title
var chapNumber string
page := int64(ctx.Page)
line := chap.toc.Add(chapNumber, chap.title, strconv.Itoa(ctx.Page), 1)
if chap.toc.showLinks {
line.SetLink(int64(ctx.Page), origX, origY)
if chap.showNumbering {
if chap.number != 0 {
chapNumber = strconv.Itoa(chap.number) + "."
}
}
if chapNumber != "" {
chapTitle = fmt.Sprintf("%s %s", chapNumber, chapTitle)
}
// Add to TOC.
if chap.includeInTOC {
line := chap.toc.Add(chapNumber, chap.title, strconv.FormatInt(page, 10), 1)
if chap.toc.showLinks {
line.SetLink(page, origX, origY)
}
}
// Add to outline.
if chap.outlineItem == nil {
chap.outlineItem = model.NewOutlineItem(
chapTitle,
model.NewOutlineDest(page-1, origX, origY),
)
chap.outline.Add(chap.outlineItem)
} else {
outlineDest := &chap.outlineItem.Dest
outlineDest.Page = page - 1
outlineDest.X = origX
outlineDest.Y = origY
}
for _, d := range chap.contents {
newBlocks, c, err := d.GeneratePageBlocks(ctx)
if err != nil {

View File

@ -43,11 +43,17 @@ type Creator struct {
finalized bool
// Controls whether a table of contents will be generated.
AddTOC bool
// The table of contents.
toc *TOC
// Controls whether a table of contents will be added.
AddTOC bool
// Controls whether outlines will be generated.
AddOutlines bool
// Outline.
outline *model.Outline
// Forms.
acroForm *model.PdfAcroForm
@ -125,6 +131,10 @@ func New() *Creator {
// Initialize creator table of contents.
c.toc = c.NewTOC("Table of Contents")
// Initialize outline.
c.AddOutlines = true
c.outline = model.NewOutline()
return c
}
@ -420,6 +430,44 @@ func (c *Creator) finalize() error {
}
}
// Account for the front page and the table of content pages.
if c.outline != nil && c.AddOutlines {
var adjustOutlineDest func(item *model.OutlineItem)
adjustOutlineDest = func(item *model.OutlineItem) {
item.Dest.Page += int64(genpages)
// Reverse the Y axis of the destination coordinates.
// The user passes in the annotation coordinates as if
// position 0, 0 is at the top left of the page.
// However, position 0, 0 in the PDF is at the bottom
// left of the page.
item.Dest.Y = c.pageHeight - item.Dest.Y
outlineItems := item.Items()
for _, outlineItem := range outlineItems {
adjustOutlineDest(outlineItem)
}
}
outlineItems := c.outline.Items()
for _, outlineItem := range outlineItems {
adjustOutlineDest(outlineItem)
}
// Add outline TOC item.
if c.AddTOC {
var tocPage int64
if hasFrontPage {
tocPage = 1
}
c.outline.Insert(0, model.NewOutlineItem(
"Table of Contents",
model.NewOutlineDest(tocPage, 0, c.pageHeight),
))
}
}
for idx, page := range c.pages {
c.setActivePage(page)
if c.drawHeaderFunc != nil {
@ -540,7 +588,12 @@ func (c *Creator) Write(ws io.Writer) error {
}
}
// Pdf Writer access hook. Can be used to encrypt, etc. via the PdfWriter instance.
// Outlines.
if c.outline != nil && c.AddOutlines {
pdfWriter.AddOutlineTree(&c.outline.ToPdfOutline().PdfOutlineTreeNode)
}
// Pdf Writer access hook. Can be used to encrypt, etc. via the PdfWriter instance.
if c.pdfWriterAccessFunc != nil {
err := c.pdfWriterAccessFunc(&pdfWriter)
if err != nil {
@ -661,7 +714,7 @@ func (c *Creator) NewStyledTOCLine(number, title, page TextChunk, level uint, st
// NewChapter creates a new chapter with the specified title as the heading.
func (c *Creator) NewChapter(title string) *Chapter {
c.chapters++
return newChapter(c.toc, title, c.chapters, c.NewTextStyle())
return newChapter(c.toc, c.outline, title, c.chapters, c.NewTextStyle())
}
// NewSubchapter creates a new Subchapter under Chapter ch with specified title.

View File

@ -10,12 +10,12 @@ import (
"strconv"
"github.com/unidoc/unidoc/common"
"github.com/unidoc/unidoc/pdf/model"
)
// Subchapter simply represents a sub chapter pertaining to a specific Chapter. It can contain
// multiple Drawables, just like a chapter.
type Subchapter struct {
chapterNum int
subchapterNum int
title string
heading *Paragraph
@ -39,6 +39,15 @@ type Subchapter struct {
// Reference to the creator's TOC.
toc *TOC
// Reference to the chapter the subchapter belongs to.
chapter *Chapter
// Reference to the outline of the creator.
outline *model.Outline
// The item of the subchapter in the outline.
outlineItem *model.OutlineItem
}
// newSubchapter creates a new Subchapter under Chapter ch with specified title.
@ -52,13 +61,14 @@ func newSubchapter(ch *Chapter, title string, style TextStyle) *Subchapter {
subchapter := &Subchapter{
subchapterNum: ch.subchapters,
chapterNum: ch.number,
title: title,
showNumbering: true,
includeInTOC: true,
heading: p,
contents: []Drawable{},
chapter: ch,
toc: ch.toc,
outline: ch.outline,
}
// Add subchapter to chapter.
@ -70,7 +80,7 @@ func newSubchapter(ch *Chapter, title string, style TextStyle) *Subchapter {
// SetShowNumbering sets a flag to indicate whether or not to show chapter numbers as part of title.
func (subchap *Subchapter) SetShowNumbering(show bool) {
if show {
heading := fmt.Sprintf("%d.%d. %s", subchap.chapterNum, subchap.subchapterNum, subchap.title)
heading := fmt.Sprintf("%d.%d. %s", subchap.chapter.number, subchap.subchapterNum, subchap.title)
subchap.heading.SetText(heading)
} else {
heading := fmt.Sprintf("%s", subchap.title)
@ -148,29 +158,51 @@ func (subchap *Subchapter) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawCo
if len(blocks) > 1 {
ctx.Page++ // did not fit - moved to next Page.
}
// Generate subchapter title and number.
subchapTitle := subchap.title
var subchapNumber string
page := int64(ctx.Page)
if subchap.showNumbering {
if subchap.chapter.number != 0 {
subchapNumber = strconv.Itoa(subchap.chapter.number)
}
if subchap.subchapterNum != 0 {
if subchapNumber != "" {
subchapNumber += "."
}
subchapNumber += strconv.Itoa(subchap.subchapterNum) + "."
}
}
if subchapNumber != "" {
subchapTitle = fmt.Sprintf("%s %s", subchapNumber, subchapTitle)
}
// Add to TOC.
if subchap.includeInTOC {
// Add to TOC.
subchapNumber := ""
if subchap.showNumbering {
if subchap.chapterNum != 0 {
subchapNumber = strconv.Itoa(subchap.chapterNum)
}
if subchap.subchapterNum != 0 {
if subchapNumber != "" {
subchapNumber += "."
}
subchapNumber += strconv.Itoa(subchap.subchapterNum) + "."
}
}
line := subchap.toc.Add(subchapNumber, subchap.title, strconv.Itoa(ctx.Page), 2)
line := subchap.toc.Add(subchapNumber, subchap.title, strconv.FormatInt(page, 10), 2)
if subchap.toc.showLinks {
line.SetLink(int64(ctx.Page), origX, origY)
line.SetLink(page, origX, origY)
}
}
// Add to outline.
if subchap.outlineItem == nil {
subchap.outlineItem = model.NewOutlineItem(
subchapTitle,
model.NewOutlineDest(page-1, origX, origY),
)
subchap.chapter.outlineItem.Add(subchap.outlineItem)
} else {
outlineDest := &subchap.outlineItem.Dest
outlineDest.Page = page - 1
outlineDest.X = origX
outlineDest.Y = origY
}
for _, d := range subchap.contents {
newBlocks, c, err := d.GeneratePageBlocks(ctx)
if err != nil {

193
pdf/model/outline.go Normal file
View File

@ -0,0 +1,193 @@
/*
* 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 (
"github.com/unidoc/unidoc/pdf/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 {
Page int64
X float64
Y float64
}
// 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,
X: x,
Y: y,
}
}
// 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 {
items []*OutlineItem
}
// 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.items = append(o.items, 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.items))
if index > l {
index = l
}
o.items = append(o.items[:index], append([]*OutlineItem{item}, o.items[index:]...)...)
}
// Items returns all children outline items.
func (o *Outline) Items() []*OutlineItem {
return o.items
}
// 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 prev *PdfOutlineItem
for _, item := range o.items {
outlineItem, _ := item.ToPdfOutlineItem()
outlineItem.Parent = &outline.PdfOutlineTreeNode
if prev != nil {
prev.Next = &outlineItem.PdfOutlineTreeNode
outlineItem.Prev = &prev.PdfOutlineTreeNode
}
outlineItems = append(outlineItems, outlineItem)
prev = outlineItem
}
// Add outline linked list properties.
lenOutlineItems := int64(len(outlineItems))
if lenOutlineItems > 0 {
outline.First = &outlineItems[0].PdfOutlineTreeNode
outline.Last = &outlineItems[lenOutlineItems-1].PdfOutlineTreeNode
outline.Count = &lenOutlineItems
}
return outline
}
// 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
Dest OutlineDest
items []*OutlineItem
}
// 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.items = append(oi.items, 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.items))
if index > l {
index = l
}
oi.items = append(oi.items[:index], append([]*OutlineItem{item}, oi.items[index:]...)...)
}
// Items returns all children outline items.
func (oi *OutlineItem) Items() []*OutlineItem {
return oi.items
}
// 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.MakeString(oi.Title)
currItem.Dest = oi.Dest.ToPdfObject()
// Create outline items.
var outlineItems []*PdfOutlineItem
var lenDescendants int64
var prev *PdfOutlineItem
for _, item := range oi.items {
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()
}

View File

@ -46,13 +46,11 @@ type PdfOutlineItem struct {
// NewPdfOutline returns an initialized PdfOutline.
func NewPdfOutline() *PdfOutline {
outline := &PdfOutline{}
container := &PdfIndirectObject{}
container.PdfObject = MakeDict()
outline.primitive = container
outline := &PdfOutline{
primitive: MakeIndirectObject(MakeDict()),
}
outline.context = outline
return outline
}
@ -65,12 +63,11 @@ func NewPdfOutlineTree() *PdfOutline {
// NewPdfOutlineItem returns an initialized PdfOutlineItem.
func NewPdfOutlineItem() *PdfOutlineItem {
outlineItem := &PdfOutlineItem{}
outlineItem := &PdfOutlineItem{
primitive: MakeIndirectObject(MakeDict()),
}
container := &PdfIndirectObject{}
container.PdfObject = MakeDict()
outlineItem.primitive = container
outlineItem.context = outlineItem
return outlineItem
}
@ -253,6 +250,10 @@ func (o *PdfOutline) ToPdfObject() PdfObject {
dict.Set("Parent", o.Parent.getOuter().GetContainingPdfObject())
}
if o.Count != nil {
dict.Set("Count", MakeInteger(*o.Count))
}
return container
}