unipdf/creator/creator.go
Adrian-George Bostan cb0166e96b
Add low level PageLabels support (#325)
* Add reader method for retriving the PageLabels entry from the catalog
* Add writer method for setting the PageLabels entry in the catalog.
* Add creator method for adding page labels for the output file
* Add creator page labels test case
* Minor page labels test case correction
2020-04-22 21:17:33 +00:00

861 lines
23 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 creator
import (
"errors"
goimage "image"
"io"
"os"
"strconv"
"github.com/unidoc/unipdf/v3/common"
"github.com/unidoc/unipdf/v3/core"
"github.com/unidoc/unipdf/v3/model"
)
// Creator is a wrapper around functionality for creating PDF reports and/or adding new
// content onto imported PDF pages, etc.
type Creator struct {
pages []*model.PdfPage
pageBlocks map[*model.PdfPage]*Block
activePage *model.PdfPage
pagesize PageSize
context DrawContext
pageMargins margins
pageWidth, pageHeight float64
// Keep track of number of chapters for indexing.
chapters int
// Hooks.
genFrontPageFunc func(args FrontpageFunctionArgs)
genTableOfContentFunc func(toc *TOC) error
drawHeaderFunc func(header *Block, args HeaderFunctionArgs)
drawFooterFunc func(footer *Block, args FooterFunctionArgs)
pdfWriterAccessFunc func(writer *model.PdfWriter) error
finalized bool
// Controls whether a table of contents will be generated.
AddTOC bool
// The table of contents.
toc *TOC
// Controls whether outlines will be generated.
AddOutlines bool
// Outline.
outline *model.Outline
// External outlines.
externalOutline *model.PdfOutlineTreeNode
// Forms.
acroForm *model.PdfAcroForm
// Page labels.
pageLabels core.PdfObject
// Optimizer.
optimizer model.Optimizer
// Default fonts used by all components instantiated through the creator.
defaultFontRegular *model.PdfFont
defaultFontBold *model.PdfFont
}
// SetForms adds an Acroform to a PDF file. Sets the specified form for writing.
func (c *Creator) SetForms(form *model.PdfAcroForm) error {
c.acroForm = form
return nil
}
// SetOutlineTree adds the specified outline tree to the PDF file generated
// by the creator. Adding an external outline tree disables the automatic
// generation of outlines done by the creator for the relevant components.
func (c *Creator) SetOutlineTree(outlineTree *model.PdfOutlineTreeNode) {
c.externalOutline = outlineTree
}
// SetPageLabels adds the specified page labels to the PDF file generated
// by the creator. See section 12.4.2 "Page Labels" (p. 382 PDF32000_2008).
// NOTE: for existing PDF files, the page label ranges object can be obtained
// using the model.PDFReader's GetPageLabels method.
func (c *Creator) SetPageLabels(pageLabels core.PdfObject) {
c.pageLabels = pageLabels
}
// FrontpageFunctionArgs holds the input arguments to a front page drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards
// compatibility.
type FrontpageFunctionArgs struct {
PageNum int
TotalPages int
}
// HeaderFunctionArgs holds the input arguments to a header drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards
// compatibility.
type HeaderFunctionArgs struct {
PageNum int
TotalPages int
}
// FooterFunctionArgs holds the input arguments to a footer drawing function.
// It is designed as a struct, so additional parameters can be added in the future with backwards
// compatibility.
type FooterFunctionArgs struct {
PageNum int
TotalPages int
}
// Margins. Can be page margins, or margins around an element.
type margins struct {
left float64
right float64
top float64
bottom float64
}
// New creates a new instance of the PDF Creator.
func New() *Creator {
c := &Creator{}
c.pages = []*model.PdfPage{}
c.pageBlocks = map[*model.PdfPage]*Block{}
c.SetPageSize(PageSizeLetter)
m := 0.1 * c.pageWidth
c.pageMargins.left = m
c.pageMargins.right = m
c.pageMargins.top = m
c.pageMargins.bottom = m
// Initialize default fonts.
var err error
c.defaultFontRegular, err = model.NewStandard14Font(model.HelveticaName)
if err != nil {
c.defaultFontRegular = model.DefaultFont()
}
c.defaultFontBold, err = model.NewStandard14Font(model.HelveticaBoldName)
if err != nil {
c.defaultFontRegular = model.DefaultFont()
}
// Initialize creator table of contents.
c.toc = c.NewTOC("Table of Contents")
// Initialize outline.
c.AddOutlines = true
c.outline = model.NewOutline()
return c
}
// SetOptimizer sets the optimizer to optimize PDF before writing.
func (c *Creator) SetOptimizer(optimizer model.Optimizer) {
c.optimizer = optimizer
}
// GetOptimizer returns current PDF optimizer.
func (c *Creator) GetOptimizer() model.Optimizer {
return c.optimizer
}
// SetPageMargins sets the page margins: left, right, top, bottom.
// The default page margins are 10% of document width.
func (c *Creator) SetPageMargins(left, right, top, bottom float64) {
c.pageMargins.left = left
c.pageMargins.right = right
c.pageMargins.top = top
c.pageMargins.bottom = bottom
}
// Width returns the current page width.
func (c *Creator) Width() float64 {
return c.pageWidth
}
// Height returns the current page height.
func (c *Creator) Height() float64 {
return c.pageHeight
}
// TOC returns the table of contents component of the creator.
func (c *Creator) TOC() *TOC {
return c.toc
}
// SetTOC sets the table of content component of the creator.
// This method should be used when building a custom table of contents.
func (c *Creator) SetTOC(toc *TOC) {
if toc == nil {
return
}
c.toc = toc
}
func (c *Creator) setActivePage(p *model.PdfPage) {
c.activePage = p
}
func (c *Creator) getActivePage() *model.PdfPage {
if c.activePage == nil {
if len(c.pages) == 0 {
return nil
}
return c.pages[len(c.pages)-1]
}
return c.activePage
}
// SetPageSize sets the Creator's page size. Pages that are added after this will be created with
// this Page size.
// Does not affect pages already created.
//
// Common page sizes are defined as constants.
// Examples:
// 1. c.SetPageSize(creator.PageSizeA4)
// 2. c.SetPageSize(creator.PageSizeA3)
// 3. c.SetPageSize(creator.PageSizeLegal)
// 4. c.SetPageSize(creator.PageSizeLetter)
//
// For custom sizes: Use the PPMM (points per mm) and PPI (points per inch) when defining those based on
// physical page sizes:
//
// Examples:
// 1. 10x15 sq. mm: SetPageSize(PageSize{10*creator.PPMM, 15*creator.PPMM}) where PPMM is points per mm.
// 2. 3x2 sq. inches: SetPageSize(PageSize{3*creator.PPI, 2*creator.PPI}) where PPI is points per inch.
//
func (c *Creator) SetPageSize(size PageSize) {
c.pagesize = size
c.pageWidth = size[0]
c.pageHeight = size[1]
// Update default margins to 10% of width.
m := 0.1 * c.pageWidth
c.pageMargins.left = m
c.pageMargins.right = m
c.pageMargins.top = m
c.pageMargins.bottom = m
}
// DrawHeader sets a function to draw a header on created output pages.
func (c *Creator) DrawHeader(drawHeaderFunc func(header *Block, args HeaderFunctionArgs)) {
c.drawHeaderFunc = drawHeaderFunc
}
// DrawFooter sets a function to draw a footer on created output pages.
func (c *Creator) DrawFooter(drawFooterFunc func(footer *Block, args FooterFunctionArgs)) {
c.drawFooterFunc = drawFooterFunc
}
// CreateFrontPage sets a function to generate a front Page.
func (c *Creator) CreateFrontPage(genFrontPageFunc func(args FrontpageFunctionArgs)) {
c.genFrontPageFunc = genFrontPageFunc
}
// CreateTableOfContents sets a function to generate table of contents.
func (c *Creator) CreateTableOfContents(genTOCFunc func(toc *TOC) error) {
c.genTableOfContentFunc = genTOCFunc
}
// Create a new Page with current parameters.
func (c *Creator) newPage() *model.PdfPage {
page := model.NewPdfPage()
width := c.pagesize[0]
height := c.pagesize[1]
bbox := model.PdfRectangle{Llx: 0, Lly: 0, Urx: width, Ury: height}
page.MediaBox = &bbox
c.pageWidth = width
c.pageHeight = height
c.initContext()
return page
}
// Initialize the drawing context, moving to upper left corner.
func (c *Creator) initContext() {
// Update context, move to upper left corner.
c.context.X = c.pageMargins.left
c.context.Y = c.pageMargins.top
c.context.Width = c.pageWidth - c.pageMargins.right - c.pageMargins.left
c.context.Height = c.pageHeight - c.pageMargins.bottom - c.pageMargins.top
c.context.PageHeight = c.pageHeight
c.context.PageWidth = c.pageWidth
c.context.Margins = c.pageMargins
}
// NewPage adds a new Page to the Creator and sets as the active Page.
func (c *Creator) NewPage() *model.PdfPage {
page := c.newPage()
c.pages = append(c.pages, page)
c.context.Page++
return page
}
// AddPage adds the specified page to the creator.
func (c *Creator) AddPage(page *model.PdfPage) error {
mbox, err := page.GetMediaBox()
if err != nil {
common.Log.Debug("Failed to get page mediabox: %v", err)
return err
}
c.context.X = mbox.Llx + c.pageMargins.left
c.context.Y = c.pageMargins.top
c.context.PageHeight = mbox.Ury - mbox.Lly
c.context.PageWidth = mbox.Urx - mbox.Llx
c.pages = append(c.pages, page)
c.context.Page++
return nil
}
// RotateDeg rotates the current active page by angle degrees. An error is returned on failure,
// which can be if there is no currently active page, or the angleDeg is not a multiple of 90 degrees.
func (c *Creator) RotateDeg(angleDeg int64) error {
page := c.getActivePage()
if page == nil {
common.Log.Debug("Fail to rotate: no page currently active")
return errors.New("no page active")
}
if angleDeg%90 != 0 {
common.Log.Debug("ERROR: Page rotation angle not a multiple of 90")
return errors.New("range check error")
}
// Do the rotation.
var rotation int64
if page.Rotate != nil {
rotation = *(page.Rotate)
}
rotation += angleDeg // Rotate by angleDeg degrees.
page.Rotate = &rotation
return nil
}
// Context returns the current drawing context.
func (c *Creator) Context() DrawContext {
return c.context
}
// Finalize renders all blocks to the creator pages. In addition, it takes care
// of adding headers and footers, as well as generating the front page,
// table of contents and outlines.
// Finalize is automatically called before writing the document out. Calling the
// method manually can be useful when adding external pages to the creator,
// using the AddPage method, as it renders all creator blocks to the added
// pages, without having to write the document out.
// NOTE: TOC and outlines are generated only if the AddTOC and AddOutlines
// fields of the creator are set to true (enabled by default). Furthermore, TOCs
// and outlines without content are skipped. TOC and outline content is
// added automatically when using the chapter component. TOCs and outlines can
// also be set externally, using the SetTOC and SetOutlineTree methods.
// Finalize should only be called once, after all draw calls have taken place,
// as it will return immediately if the creator instance has been finalized.
func (c *Creator) Finalize() error {
if c.finalized {
return nil
}
totPages := len(c.pages)
// Estimate number of additional generated pages and update TOC.
genpages := 0
if c.genFrontPageFunc != nil {
genpages++
}
if c.AddTOC {
c.initContext()
c.context.Page = genpages + 1
if c.genTableOfContentFunc != nil {
if err := c.genTableOfContentFunc(c.toc); err != nil {
return err
}
}
// Make an estimate of the number of pages.
blocks, _, err := c.toc.GeneratePageBlocks(c.context)
if err != nil {
common.Log.Debug("Failed to generate blocks: %v", err)
return err
}
genpages += len(blocks)
// Update the table of content Page numbers, accounting for front Page and TOC.
lines := c.toc.Lines()
for _, line := range lines {
pageNum, err := strconv.Atoi(line.Page.Text)
if err != nil {
continue
}
line.Page.Text = strconv.Itoa(pageNum + genpages)
}
}
hasFrontPage := false
// Generate the front Page.
if c.genFrontPageFunc != nil {
totPages++
p := c.newPage()
// Place at front.
c.pages = append([]*model.PdfPage{p}, c.pages...)
c.setActivePage(p)
args := FrontpageFunctionArgs{
PageNum: 1,
TotalPages: totPages,
}
c.genFrontPageFunc(args)
hasFrontPage = true
}
if c.AddTOC {
c.initContext()
if c.genTableOfContentFunc != nil {
if err := c.genTableOfContentFunc(c.toc); err != nil {
common.Log.Debug("Error generating TOC: %v", err)
return err
}
}
// Account for the front page and the table of content pages.
lines := c.toc.Lines()
for _, line := range lines {
line.linkPage += int64(genpages)
}
// Create TOC pages.
var tocpages []*model.PdfPage
blocks, _, _ := c.toc.GeneratePageBlocks(c.context)
for _, block := range blocks {
block.SetPos(0, 0)
totPages++
p := c.newPage()
// Place at front.
tocpages = append(tocpages, p)
c.setActivePage(p)
c.Draw(block)
}
if hasFrontPage {
front := c.pages[0]
rest := c.pages[1:]
c.pages = append([]*model.PdfPage{front}, tocpages...)
c.pages = append(c.pages, rest...)
} else {
c.pages = append(tocpages, c.pages...)
}
}
// 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)
// Draw page header.
if c.drawHeaderFunc != nil {
// Prepare a block to draw on.
// Header is drawn on the top of the page. Has width of the page, but height limited to
// the page margin top height.
headerBlock := NewBlock(c.pageWidth, c.pageMargins.top)
args := HeaderFunctionArgs{
PageNum: idx + 1,
TotalPages: totPages,
}
c.drawHeaderFunc(headerBlock, args)
headerBlock.SetPos(0, 0)
if err := c.Draw(headerBlock); err != nil {
common.Log.Debug("ERROR: drawing header: %v", err)
return err
}
}
// Draw page footer.
if c.drawFooterFunc != nil {
// Prepare a block to draw on.
// Footer is drawn on the bottom of the page. Has width of the page, but height limited
// to the page margin bottom height.
footerBlock := NewBlock(c.pageWidth, c.pageMargins.bottom)
args := FooterFunctionArgs{
PageNum: idx + 1,
TotalPages: totPages,
}
c.drawFooterFunc(footerBlock, args)
footerBlock.SetPos(0, c.pageHeight-footerBlock.height)
if err := c.Draw(footerBlock); err != nil {
common.Log.Debug("ERROR: drawing footer: %v", err)
return err
}
}
// Draw page blocks.
block, ok := c.pageBlocks[page]
if !ok {
continue
}
if err := block.drawToPage(page); err != nil {
common.Log.Debug("ERROR: drawing page %d blocks: %v", idx+1, err)
return err
}
}
c.finalized = true
return nil
}
// MoveTo moves the drawing context to absolute coordinates (x, y).
func (c *Creator) MoveTo(x, y float64) {
c.context.X = x
c.context.Y = y
}
// MoveX moves the drawing context to absolute position x.
func (c *Creator) MoveX(x float64) {
c.context.X = x
}
// MoveY moves the drawing context to absolute position y.
func (c *Creator) MoveY(y float64) {
c.context.Y = y
}
// MoveRight moves the drawing context right by relative displacement dx (negative goes left).
func (c *Creator) MoveRight(dx float64) {
c.context.X += dx
}
// MoveDown moves the drawing context down by relative displacement dy (negative goes up).
func (c *Creator) MoveDown(dy float64) {
c.context.Y += dy
}
// Draw processes the specified Drawable widget and generates blocks that can
// be rendered to the output document. The generated blocks can span over one
// or more pages. Additional pages are added if the contents go over the current
// page. Each generated block is assigned to the creator page it will be
// rendered to. In order to render the generated blocks to the creator pages,
// call Finalize, Write or WriteToFile.
func (c *Creator) Draw(d Drawable) error {
if c.getActivePage() == nil {
// Add a new Page if none added already.
c.NewPage()
}
blocks, ctx, err := d.GeneratePageBlocks(c.context)
if err != nil {
return err
}
for idx, block := range blocks {
if idx > 0 {
c.NewPage()
}
page := c.getActivePage()
if pageBlock, ok := c.pageBlocks[page]; ok {
if err := pageBlock.mergeBlocks(block); err != nil {
return err
}
if err := mergeResources(block.resources, pageBlock.resources); err != nil {
return err
}
} else {
c.pageBlocks[page] = block
}
}
// Inner elements can affect X, Y position and available height.
c.context.X = ctx.X
c.context.Y = ctx.Y
c.context.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom
return nil
}
// Write output of creator to io.Writer interface.
func (c *Creator) Write(ws io.Writer) error {
if err := c.Finalize(); err != nil {
return err
}
pdfWriter := model.NewPdfWriter()
pdfWriter.SetOptimizer(c.optimizer)
// Form fields.
if c.acroForm != nil {
err := pdfWriter.SetForms(c.acroForm)
if err != nil {
common.Log.Debug("Failure: %v", err)
return err
}
}
// Outlines.
if c.externalOutline != nil {
pdfWriter.AddOutlineTree(c.externalOutline)
} else if c.outline != nil && c.AddOutlines {
pdfWriter.AddOutlineTree(&c.outline.ToPdfOutline().PdfOutlineTreeNode)
}
// Page labels.
if c.pageLabels != nil {
if err := pdfWriter.SetPageLabels(c.pageLabels); err != nil {
common.Log.Debug("ERROR: Could not set page labels: %v", err)
return err
}
}
// 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 {
common.Log.Debug("Failure: %v", err)
return err
}
}
for _, page := range c.pages {
err := pdfWriter.AddPage(page)
if err != nil {
common.Log.Error("Failed to add Page: %v", err)
return err
}
}
err := pdfWriter.Write(ws)
if err != nil {
return err
}
return nil
}
// SetPdfWriterAccessFunc sets a PdfWriter access function/hook.
// Exposes the PdfWriter just prior to writing the PDF. Can be used to encrypt the output PDF, etc.
//
// Example of encrypting with a user/owner password "password"
// Prior to calling c.WriteFile():
//
// c.SetPdfWriterAccessFunc(func(w *model.PdfWriter) error {
// userPass := []byte("password")
// ownerPass := []byte("password")
// err := w.Encrypt(userPass, ownerPass, nil)
// return err
// })
//
func (c *Creator) SetPdfWriterAccessFunc(pdfWriterAccessFunc func(writer *model.PdfWriter) error) {
c.pdfWriterAccessFunc = pdfWriterAccessFunc
}
// WriteToFile writes the Creator output to file specified by path.
func (c *Creator) WriteToFile(outputPath string) error {
fWrite, err := os.Create(outputPath)
if err != nil {
return err
}
defer fWrite.Close()
return c.Write(fWrite)
}
/*
Component creation methods.
*/
// NewTextStyle creates a new text style object which can be used to style
// chunks of text.
// Default attributes:
// Font: Helvetica
// Font size: 10
// Encoding: WinAnsiEncoding
// Text color: black
func (c *Creator) NewTextStyle() TextStyle {
return newTextStyle(c.defaultFontRegular)
}
// NewParagraph creates a new text paragraph.
// Default attributes:
// Font: Helvetica,
// Font size: 10
// Encoding: WinAnsiEncoding
// Wrap: enabled
// Text color: black
func (c *Creator) NewParagraph(text string) *Paragraph {
return newParagraph(text, c.NewTextStyle())
}
// NewStyledParagraph creates a new styled paragraph.
// Default attributes:
// Font: Helvetica,
// Font size: 10
// Encoding: WinAnsiEncoding
// Wrap: enabled
// Text color: black
func (c *Creator) NewStyledParagraph() *StyledParagraph {
return newStyledParagraph(c.NewTextStyle())
}
// NewTable create a new Table with a specified number of columns.
func (c *Creator) NewTable(cols int) *Table {
return newTable(cols)
}
// NewDivision returns a new Division container component.
func (c *Creator) NewDivision() *Division {
return newDivision()
}
// NewTOC creates a new table of contents.
func (c *Creator) NewTOC(title string) *TOC {
headingStyle := c.NewTextStyle()
headingStyle.Font = c.defaultFontBold
return newTOC(title, c.NewTextStyle(), headingStyle)
}
// NewTOCLine creates a new table of contents line with the default style.
func (c *Creator) NewTOCLine(number, title, page string, level uint) *TOCLine {
return newTOCLine(number, title, page, level, c.NewTextStyle())
}
// NewStyledTOCLine creates a new table of contents line with the provided style.
func (c *Creator) NewStyledTOCLine(number, title, page TextChunk, level uint, style TextStyle) *TOCLine {
return newStyledTOCLine(number, title, page, level, style)
}
// NewChapter creates a new chapter with the specified title as the heading.
func (c *Creator) NewChapter(title string) *Chapter {
c.chapters++
style := c.NewTextStyle()
style.FontSize = 16
return newChapter(nil, c.toc, c.outline, title, c.chapters, style)
}
// NewInvoice returns an instance of an empty invoice.
func (c *Creator) NewInvoice() *Invoice {
headingStyle := c.NewTextStyle()
headingStyle.Font = c.defaultFontBold
return newInvoice(c.NewTextStyle(), headingStyle)
}
// NewList creates a new list.
func (c *Creator) NewList() *List {
return newList(c.NewTextStyle())
}
// NewRectangle creates a new Rectangle with default parameters
// with left corner at (x,y) and width, height as specified.
func (c *Creator) NewRectangle(x, y, width, height float64) *Rectangle {
return newRectangle(x, y, width, height)
}
// NewPageBreak create a new page break.
func (c *Creator) NewPageBreak() *PageBreak {
return newPageBreak()
}
// NewLine creates a new Line with default parameters between (x1,y1) to (x2,y2).
func (c *Creator) NewLine(x1, y1, x2, y2 float64) *Line {
return newLine(x1, y1, x2, y2)
}
// NewFilledCurve returns a instance of filled curve.
func (c *Creator) NewFilledCurve() *FilledCurve {
return newFilledCurve()
}
// NewEllipse creates a new ellipse centered at (xc,yc) with a width and height specified.
func (c *Creator) NewEllipse(xc, yc, width, height float64) *Ellipse {
return newEllipse(xc, yc, width, height)
}
// NewCurve returns new instance of Curve between points (x1,y1) and (x2, y2) with control point (cx,cy).
func (c *Creator) NewCurve(x1, y1, cx, cy, x2, y2 float64) *Curve {
return newCurve(x1, y1, cx, cy, x2, y2)
}
// NewImage create a new image from a unidoc image (model.Image).
func (c *Creator) NewImage(img *model.Image) (*Image, error) {
return newImage(img)
}
// NewImageFromData creates an Image from image data.
func (c *Creator) NewImageFromData(data []byte) (*Image, error) {
return newImageFromData(data)
}
// NewImageFromFile creates an Image from a file.
func (c *Creator) NewImageFromFile(path string) (*Image, error) {
return newImageFromFile(path)
}
// NewImageFromGoImage creates an Image from a go image.Image data structure.
func (c *Creator) NewImageFromGoImage(goimg goimage.Image) (*Image, error) {
return newImageFromGoImage(goimg)
}