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 }