github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/creator/creator.go (about)

     1  /*
     2   * This file is subject to the terms and conditions defined in
     3   * file 'LICENSE.md', which is part of this source code package.
     4   */
     5  
     6  package creator
     7  
     8  import (
     9  	"errors"
    10  	"io"
    11  	"os"
    12  
    13  	"github.com/unidoc/unidoc/common"
    14  	"github.com/unidoc/unidoc/pdf/model"
    15  )
    16  
    17  // Creator is a wrapper around functionality for creating PDF reports and/or adding new
    18  // content onto imported PDF pages, etc.
    19  type Creator struct {
    20  	pages      []*model.PdfPage
    21  	activePage *model.PdfPage
    22  
    23  	pagesize PageSize
    24  
    25  	context DrawContext
    26  
    27  	pageMargins margins
    28  
    29  	pageWidth, pageHeight float64
    30  
    31  	// Keep track of number of chapters for indexing.
    32  	chapters int
    33  
    34  	// Hooks.
    35  	genFrontPageFunc      func(args FrontpageFunctionArgs)
    36  	genTableOfContentFunc func(toc *TableOfContents) (*Chapter, error)
    37  	drawHeaderFunc        func(header *Block, args HeaderFunctionArgs)
    38  	drawFooterFunc        func(footer *Block, args FooterFunctionArgs)
    39  	pdfWriterAccessFunc   func(writer *model.PdfWriter) error
    40  
    41  	finalized bool
    42  
    43  	toc *TableOfContents
    44  
    45  	// Forms.
    46  	acroForm *model.PdfAcroForm
    47  }
    48  
    49  // SetForms Add Acroforms to a PDF file.  Sets the specified form for writing.
    50  func (c *Creator) SetForms(form *model.PdfAcroForm) error {
    51  	c.acroForm = form
    52  	return nil
    53  }
    54  
    55  // FrontpageFunctionArgs holds the input arguments to a front page drawing function.
    56  // It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
    57  type FrontpageFunctionArgs struct {
    58  	PageNum    int
    59  	TotalPages int
    60  }
    61  
    62  // HeaderFunctionArgs holds the input arguments to a header drawing function.
    63  // It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
    64  type HeaderFunctionArgs struct {
    65  	PageNum    int
    66  	TotalPages int
    67  }
    68  
    69  // FooterFunctionArgs holds the input arguments to a footer drawing function.
    70  // It is designed as a struct, so additional parameters can be added in the future with backwards compatibility.
    71  type FooterFunctionArgs struct {
    72  	PageNum    int
    73  	TotalPages int
    74  }
    75  
    76  // Margins.  Can be page margins, or margins around an element.
    77  type margins struct {
    78  	left   float64
    79  	right  float64
    80  	top    float64
    81  	bottom float64
    82  }
    83  
    84  // New creates a new instance of the PDF Creator.
    85  func New() *Creator {
    86  	c := &Creator{}
    87  	c.pages = []*model.PdfPage{}
    88  	c.SetPageSize(PageSizeLetter)
    89  
    90  	m := 0.1 * c.pageWidth
    91  	c.pageMargins.left = m
    92  	c.pageMargins.right = m
    93  	c.pageMargins.top = m
    94  	c.pageMargins.bottom = m
    95  
    96  	c.toc = newTableOfContents()
    97  
    98  	return c
    99  }
   100  
   101  // SetPageMargins sets the page margins: left, right, top, bottom.
   102  // The default page margins are 10% of document width.
   103  func (c *Creator) SetPageMargins(left, right, top, bottom float64) {
   104  	c.pageMargins.left = left
   105  	c.pageMargins.right = right
   106  	c.pageMargins.top = top
   107  	c.pageMargins.bottom = bottom
   108  }
   109  
   110  // Width returns the current page width.
   111  func (c *Creator) Width() float64 {
   112  	return c.pageWidth
   113  }
   114  
   115  // Height returns the current page height.
   116  func (c *Creator) Height() float64 {
   117  	return c.pageHeight
   118  }
   119  
   120  func (c *Creator) setActivePage(p *model.PdfPage) {
   121  	c.activePage = p
   122  }
   123  
   124  func (c *Creator) getActivePage() *model.PdfPage {
   125  	if c.activePage == nil {
   126  		if len(c.pages) == 0 {
   127  			return nil
   128  		}
   129  		return c.pages[len(c.pages)-1]
   130  	}
   131  	return c.activePage
   132  }
   133  
   134  // SetPageSize sets the Creator's page size.  Pages that are added after this will be created with this Page size.
   135  // Does not affect pages already created.
   136  //
   137  // Common page sizes are defined as constants.
   138  // Examples:
   139  // 1. c.SetPageSize(creator.PageSizeA4)
   140  // 2. c.SetPageSize(creator.PageSizeA3)
   141  // 3. c.SetPageSize(creator.PageSizeLegal)
   142  // 4. c.SetPageSize(creator.PageSizeLetter)
   143  //
   144  // For custom sizes: Use the PPMM (points per mm) and PPI (points per inch) when defining those based on
   145  // physical page sizes:
   146  //
   147  // Examples:
   148  // 1. 10x15 sq. mm: SetPageSize(PageSize{10*creator.PPMM, 15*creator.PPMM}) where PPMM is points per mm.
   149  // 2. 3x2 sq. inches: SetPageSize(PageSize{3*creator.PPI, 2*creator.PPI}) where PPI is points per inch.
   150  //
   151  func (c *Creator) SetPageSize(size PageSize) {
   152  	c.pagesize = size
   153  
   154  	c.pageWidth = size[0]
   155  	c.pageHeight = size[1]
   156  
   157  	// Update default margins to 10% of width.
   158  	m := 0.1 * c.pageWidth
   159  	c.pageMargins.left = m
   160  	c.pageMargins.right = m
   161  	c.pageMargins.top = m
   162  	c.pageMargins.bottom = m
   163  }
   164  
   165  // DrawHeader sets a function to draw a header on created output pages.
   166  func (c *Creator) DrawHeader(drawHeaderFunc func(header *Block, args HeaderFunctionArgs)) {
   167  	c.drawHeaderFunc = drawHeaderFunc
   168  }
   169  
   170  // DrawFooter sets a function to draw a footer on created output pages.
   171  func (c *Creator) DrawFooter(drawFooterFunc func(footer *Block, args FooterFunctionArgs)) {
   172  	c.drawFooterFunc = drawFooterFunc
   173  }
   174  
   175  // CreateFrontPage sets a function to generate a front Page.
   176  func (c *Creator) CreateFrontPage(genFrontPageFunc func(args FrontpageFunctionArgs)) {
   177  	c.genFrontPageFunc = genFrontPageFunc
   178  }
   179  
   180  // CreateTableOfContents sets a function to generate table of contents.
   181  func (c *Creator) CreateTableOfContents(genTOCFunc func(toc *TableOfContents) (*Chapter, error)) {
   182  	c.genTableOfContentFunc = genTOCFunc
   183  }
   184  
   185  // Create a new Page with current parameters.
   186  func (c *Creator) newPage() *model.PdfPage {
   187  	page := model.NewPdfPage()
   188  
   189  	width := c.pagesize[0]
   190  	height := c.pagesize[1]
   191  
   192  	bbox := model.PdfRectangle{Llx: 0, Lly: 0, Urx: width, Ury: height}
   193  	page.MediaBox = &bbox
   194  
   195  	c.pageWidth = width
   196  	c.pageHeight = height
   197  
   198  	c.initContext()
   199  
   200  	return page
   201  }
   202  
   203  // Initialize the drawing context, moving to upper left corner.
   204  func (c *Creator) initContext() {
   205  	// Update context, move to upper left corner.
   206  	c.context.X = c.pageMargins.left
   207  	c.context.Y = c.pageMargins.top
   208  	c.context.Width = c.pageWidth - c.pageMargins.right - c.pageMargins.left
   209  	c.context.Height = c.pageHeight - c.pageMargins.bottom - c.pageMargins.top
   210  	c.context.PageHeight = c.pageHeight
   211  	c.context.PageWidth = c.pageWidth
   212  	c.context.Margins = c.pageMargins
   213  }
   214  
   215  // NewPage adds a new Page to the Creator and sets as the active Page.
   216  func (c *Creator) NewPage() {
   217  	page := c.newPage()
   218  	c.pages = append(c.pages, page)
   219  	c.context.Page++
   220  }
   221  
   222  // AddPage adds the specified page to the creator.
   223  func (c *Creator) AddPage(page *model.PdfPage) error {
   224  	mbox, err := page.GetMediaBox()
   225  	if err != nil {
   226  		common.Log.Debug("Failed to get page mediabox: %v", err)
   227  		return err
   228  	}
   229  
   230  	c.context.X = mbox.Llx + c.pageMargins.left
   231  	c.context.Y = c.pageMargins.top
   232  	c.context.PageHeight = mbox.Ury - mbox.Lly
   233  	c.context.PageWidth = mbox.Urx - mbox.Llx
   234  
   235  	c.pages = append(c.pages, page)
   236  	c.context.Page++
   237  
   238  	return nil
   239  }
   240  
   241  // RotateDeg rotates the current active page by angle degrees.  An error is returned on failure, which can be
   242  // if there is no currently active page, or the angleDeg is not a multiple of 90 degrees.
   243  func (c *Creator) RotateDeg(angleDeg int64) error {
   244  	page := c.getActivePage()
   245  	if page == nil {
   246  		common.Log.Debug("Fail to rotate: no page currently active")
   247  		return errors.New("No page active")
   248  	}
   249  	if angleDeg%90 != 0 {
   250  		common.Log.Debug("Error: Page rotation angle not a multiple of 90")
   251  		return errors.New("Range check error")
   252  	}
   253  
   254  	// Do the rotation.
   255  	var rotation int64 = 0
   256  	if page.Rotate != nil {
   257  		rotation = *(page.Rotate)
   258  	}
   259  	rotation += angleDeg // Rotate by angleDeg degrees.
   260  	page.Rotate = &rotation
   261  
   262  	return nil
   263  }
   264  
   265  // Context returns the current drawing context.
   266  func (c *Creator) Context() DrawContext {
   267  	return c.context
   268  }
   269  
   270  // Call before writing out.  Takes care of adding headers and footers, as well as generating front Page and
   271  // table of contents.
   272  func (c *Creator) finalize() error {
   273  	totPages := len(c.pages)
   274  
   275  	// Estimate number of additional generated pages and update TOC.
   276  	genpages := 0
   277  	if c.genFrontPageFunc != nil {
   278  		genpages++
   279  	}
   280  	if c.genTableOfContentFunc != nil {
   281  		c.initContext()
   282  		c.context.Page = genpages + 1
   283  		ch, err := c.genTableOfContentFunc(c.toc)
   284  		if err != nil {
   285  			return err
   286  		}
   287  
   288  		// Make an estimate of the number of pages.
   289  		blocks, _, err := ch.GeneratePageBlocks(c.context)
   290  		if err != nil {
   291  			common.Log.Debug("Failed to generate blocks: %v", err)
   292  			return err
   293  		}
   294  		genpages += len(blocks)
   295  
   296  		// Update the table of content Page numbers, accounting for front Page and TOC.
   297  		for idx := range c.toc.entries {
   298  			c.toc.entries[idx].PageNumber += genpages
   299  		}
   300  
   301  		// Remove the TOC chapter entry.
   302  		c.toc.entries = c.toc.entries[:len(c.toc.entries)-1]
   303  	}
   304  
   305  	hasFrontPage := false
   306  	// Generate the front Page.
   307  	if c.genFrontPageFunc != nil {
   308  		totPages++
   309  		p := c.newPage()
   310  		// Place at front.
   311  		c.pages = append([]*model.PdfPage{p}, c.pages...)
   312  		c.setActivePage(p)
   313  
   314  		args := FrontpageFunctionArgs{
   315  			PageNum:    1,
   316  			TotalPages: totPages,
   317  		}
   318  		c.genFrontPageFunc(args)
   319  		hasFrontPage = true
   320  	}
   321  
   322  	if c.genTableOfContentFunc != nil {
   323  		c.initContext()
   324  		ch, err := c.genTableOfContentFunc(c.toc)
   325  		if err != nil {
   326  			common.Log.Debug("Error generating TOC: %v", err)
   327  			return err
   328  		}
   329  		ch.SetShowNumbering(false)
   330  		ch.SetIncludeInTOC(false)
   331  
   332  		blocks, _, _ := ch.GeneratePageBlocks(c.context)
   333  		tocpages := []*model.PdfPage{}
   334  		for _, block := range blocks {
   335  			block.SetPos(0, 0)
   336  			totPages++
   337  			p := c.newPage()
   338  			// Place at front.
   339  			tocpages = append(tocpages, p)
   340  			c.setActivePage(p)
   341  			c.Draw(block)
   342  		}
   343  
   344  		if hasFrontPage {
   345  			front := c.pages[0]
   346  			rest := c.pages[1:]
   347  			c.pages = append([]*model.PdfPage{front}, tocpages...)
   348  			c.pages = append(c.pages, rest...)
   349  		} else {
   350  			c.pages = append(tocpages, c.pages...)
   351  		}
   352  
   353  	}
   354  
   355  	for idx, page := range c.pages {
   356  		c.setActivePage(page)
   357  		if c.drawHeaderFunc != nil {
   358  			// Prepare a block to draw on.
   359  			// Header is drawn on the top of the page. Has width of the page, but height limited to the page
   360  			// margin top height.
   361  			headerBlock := NewBlock(c.pageWidth, c.pageMargins.top)
   362  			args := HeaderFunctionArgs{
   363  				PageNum:    idx + 1,
   364  				TotalPages: totPages,
   365  			}
   366  			c.drawHeaderFunc(headerBlock, args)
   367  			headerBlock.SetPos(0, 0)
   368  			err := c.Draw(headerBlock)
   369  			if err != nil {
   370  				common.Log.Debug("Error drawing header: %v", err)
   371  				return err
   372  			}
   373  
   374  		}
   375  		if c.drawFooterFunc != nil {
   376  			// Prepare a block to draw on.
   377  			// Footer is drawn on the bottom of the page. Has width of the page, but height limited to the page
   378  			// margin bottom height.
   379  			footerBlock := NewBlock(c.pageWidth, c.pageMargins.bottom)
   380  			args := FooterFunctionArgs{
   381  				PageNum:    idx + 1,
   382  				TotalPages: totPages,
   383  			}
   384  			c.drawFooterFunc(footerBlock, args)
   385  			footerBlock.SetPos(0, c.pageHeight-footerBlock.height)
   386  			err := c.Draw(footerBlock)
   387  			if err != nil {
   388  				common.Log.Debug("Error drawing footer: %v", err)
   389  				return err
   390  			}
   391  		}
   392  	}
   393  
   394  	c.finalized = true
   395  
   396  	return nil
   397  }
   398  
   399  // MoveTo moves the drawing context to absolute coordinates (x, y).
   400  func (c *Creator) MoveTo(x, y float64) {
   401  	c.context.X = x
   402  	c.context.Y = y
   403  }
   404  
   405  // MoveX moves the drawing context to absolute position x.
   406  func (c *Creator) MoveX(x float64) {
   407  	c.context.X = x
   408  }
   409  
   410  // MoveY moves the drawing context to absolute position y.
   411  func (c *Creator) MoveY(y float64) {
   412  	c.context.Y = y
   413  }
   414  
   415  // MoveRight moves the drawing context right by relative displacement dx (negative goes left).
   416  func (c *Creator) MoveRight(dx float64) {
   417  	c.context.X += dx
   418  }
   419  
   420  // MoveDown moves the drawing context down by relative displacement dy (negative goes up).
   421  func (c *Creator) MoveDown(dy float64) {
   422  	c.context.Y += dy
   423  }
   424  
   425  // Draw draws the Drawable widget to the document.  This can span over 1 or more pages. Additional pages are added if
   426  // the contents go over the current Page.
   427  func (c *Creator) Draw(d Drawable) error {
   428  	if c.getActivePage() == nil {
   429  		// Add a new Page if none added already.
   430  		c.NewPage()
   431  	}
   432  
   433  	blocks, ctx, err := d.GeneratePageBlocks(c.context)
   434  	if err != nil {
   435  		return err
   436  	}
   437  
   438  	for idx, blk := range blocks {
   439  		if idx > 0 {
   440  			c.NewPage()
   441  		}
   442  
   443  		p := c.getActivePage()
   444  		err := blk.drawToPage(p)
   445  		if err != nil {
   446  			return err
   447  		}
   448  	}
   449  
   450  	// Inner elements can affect X, Y position and available height.
   451  	c.context.X = ctx.X
   452  	c.context.Y = ctx.Y
   453  	c.context.Height = ctx.PageHeight - ctx.Y - ctx.Margins.bottom
   454  
   455  	return nil
   456  }
   457  
   458  // Write output of creator to io.WriteSeeker interface.
   459  func (c *Creator) Write(ws io.WriteSeeker) error {
   460  	if !c.finalized {
   461  		c.finalize()
   462  	}
   463  
   464  	pdfWriter := model.NewPdfWriter()
   465  	// Form fields.
   466  	if c.acroForm != nil {
   467  		errF := pdfWriter.SetForms(c.acroForm)
   468  		if errF != nil {
   469  			common.Log.Debug("Failure: %v", errF)
   470  			return errF
   471  		}
   472  	}
   473  
   474  	// Pdf Writer access hook.  Can be used to encrypt, etc. via the PdfWriter instance.
   475  	if c.pdfWriterAccessFunc != nil {
   476  		err := c.pdfWriterAccessFunc(&pdfWriter)
   477  		if err != nil {
   478  			common.Log.Debug("Failure: %v", err)
   479  			return err
   480  		}
   481  	}
   482  
   483  	for _, page := range c.pages {
   484  		err := pdfWriter.AddPage(page)
   485  		if err != nil {
   486  			common.Log.Error("Failed to add Page: %s", err)
   487  			return err
   488  		}
   489  	}
   490  
   491  	err := pdfWriter.Write(ws)
   492  	if err != nil {
   493  		return err
   494  	}
   495  
   496  	return nil
   497  }
   498  
   499  // SetPdfWriterAccessFunc sets a PdfWriter access function/hook.
   500  // Exposes the PdfWriter just prior to writing the PDF.  Can be used to encrypt the output PDF, etc.
   501  //
   502  // Example of encrypting with a user/owner password "password"
   503  // Prior to calling c.WriteFile():
   504  //
   505  // c.SetPdfWriterAccessFunc(func(w *model.PdfWriter) error {
   506  //	userPass := []byte("password")
   507  //	ownerPass := []byte("password")
   508  //	err := w.Encrypt(userPass, ownerPass, nil)
   509  //	return err
   510  // })
   511  //
   512  func (c *Creator) SetPdfWriterAccessFunc(pdfWriterAccessFunc func(writer *model.PdfWriter) error) {
   513  	c.pdfWriterAccessFunc = pdfWriterAccessFunc
   514  }
   515  
   516  // WriteToFile writes the Creator output to file specified by path.
   517  func (c *Creator) WriteToFile(outputPath string) error {
   518  	fWrite, err := os.Create(outputPath)
   519  	if err != nil {
   520  		return err
   521  	}
   522  
   523  	defer fWrite.Close()
   524  
   525  	err = c.Write(fWrite)
   526  	if err != nil {
   527  		return err
   528  	}
   529  
   530  	return nil
   531  }