diff --git a/pdf/creator/chapters.go b/pdf/creator/chapters.go index afe3e8f7..bff39f00 100644 --- a/pdf/creator/chapters.go +++ b/pdf/creator/chapters.go @@ -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 { diff --git a/pdf/creator/creator.go b/pdf/creator/creator.go index 118e4a21..6d0c55e5 100644 --- a/pdf/creator/creator.go +++ b/pdf/creator/creator.go @@ -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. diff --git a/pdf/creator/subchapter.go b/pdf/creator/subchapter.go index f0839888..aaadfa93 100644 --- a/pdf/creator/subchapter.go +++ b/pdf/creator/subchapter.go @@ -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 { diff --git a/pdf/model/outline.go b/pdf/model/outline.go new file mode 100644 index 00000000..72186596 --- /dev/null +++ b/pdf/model/outline.go @@ -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() +} diff --git a/pdf/model/outlines.go b/pdf/model/outlines.go index e1fa17c7..612a82bf 100644 --- a/pdf/model/outlines.go +++ b/pdf/model/outlines.go @@ -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 }