mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-01 22:17:29 +08:00
Merge pull request #321 from adrg/pdf-outlines
Add Creator support for outlines
This commit is contained in:
commit
33d17ce9aa
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
193
pdf/model/outline.go
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user