github.com/pdfcpu/pdfcpu@v0.11.1/pkg/pdfcpu/nup.go (about) 1 /* 2 Copyright 2018 The pdfcpu Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package pdfcpu 18 19 import ( 20 "bytes" 21 "fmt" 22 "io" 23 "os" 24 "sort" 25 "strconv" 26 "strings" 27 28 "github.com/pdfcpu/pdfcpu/pkg/filter" 29 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" 30 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw" 31 pdffont "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font" 32 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" 33 "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" 34 "github.com/pkg/errors" 35 ) 36 37 var ( 38 errInvalidGridDims = errors.New("pdfcpu grid: dimensions must be: m > 0, n > 0") 39 errInvalidNUpConfig = errors.New("pdfcpu: invalid configuration string") 40 ) 41 42 var ( 43 NUpValues = []int{2, 3, 4, 6, 8, 9, 12, 16} 44 nUpDims = map[int]types.Dim{ 45 2: {Width: 2, Height: 1}, 46 3: {Width: 3, Height: 1}, 47 4: {Width: 2, Height: 2}, 48 6: {Width: 3, Height: 2}, 49 8: {Width: 4, Height: 2}, 50 9: {Width: 3, Height: 3}, 51 12: {Width: 4, Height: 3}, 52 16: {Width: 4, Height: 4}, 53 } 54 ) 55 56 type nUpParamMap map[string]func(string, *model.NUp) error 57 58 var nupParamMap = nUpParamMap{ 59 "dimensions": parseDimensionsNUp, 60 "formsize": parsePageFormatNUp, 61 "papersize": parsePageFormatNUp, 62 "orientation": parseOrientation, 63 "border": parseElementBorder, 64 "cropboxborder": parseElementBorderOnCropbox, 65 "margin": parseElementMargin, 66 "backgroundcolor": parseSheetBackgroundColor, 67 "bgcolor": parseSheetBackgroundColor, 68 "guides": parseBookletGuides, 69 "multifolio": parseBookletMultifolio, 70 "foliosize": parseBookletFolioSize, 71 "btype": parseBookletType, 72 "binding": parseBookletBinding, 73 "enforce": parseEnforce, 74 } 75 76 // Handle applies parameter completion and if successful 77 // parses the parameter values into import. 78 func (m nUpParamMap) Handle(paramPrefix, paramValueStr string, nup *model.NUp) error { 79 var param string 80 81 // Completion support 82 for k := range m { 83 if !strings.HasPrefix(k, strings.ToLower(paramPrefix)) { 84 continue 85 } 86 if len(param) > 0 { 87 return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) 88 } 89 param = k 90 } 91 92 if param == "" { 93 return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix) 94 } 95 96 return m[param](paramValueStr, nup) 97 } 98 99 func parsePageFormatNUp(s string, nup *model.NUp) (err error) { 100 if nup.UserDim { 101 return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") 102 } 103 nup.PageDim, nup.PageSize, err = types.ParsePageFormat(s) 104 nup.UserDim = true 105 return err 106 } 107 108 func parseDimensionsNUp(s string, nup *model.NUp) (err error) { 109 if nup.UserDim { 110 return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed") 111 } 112 nup.PageDim, nup.PageSize, err = ParsePageDim(s, nup.InpUnit) 113 nup.UserDim = true 114 115 return err 116 } 117 118 func parseOrientation(s string, nup *model.NUp) error { 119 switch s { 120 case "rd": 121 nup.Orient = model.RightDown 122 case "dr": 123 nup.Orient = model.DownRight 124 case "ld": 125 nup.Orient = model.LeftDown 126 case "dl": 127 nup.Orient = model.DownLeft 128 default: 129 return errors.Errorf("pdfcpu: unknown nUp orientation: %s", s) 130 } 131 132 return nil 133 } 134 135 func parseEnforce(s string, nup *model.NUp) error { 136 switch strings.ToLower(s) { 137 case "on", "true", "t": 138 nup.Enforce = true 139 case "off", "false", "f": 140 nup.Enforce = false 141 default: 142 return errors.New("pdfcpu: enforce best-fit orientation of content, please provide one of: on/off true/false") 143 } 144 145 return nil 146 } 147 148 func parseElementBorder(s string, nup *model.NUp) error { 149 switch strings.ToLower(s) { 150 case "on", "true", "t": 151 nup.Border = true 152 case "off", "false", "f": 153 nup.Border = false 154 default: 155 return errors.New("pdfcpu: nUp border, please provide one of: on/off true/false t/f") 156 } 157 158 return nil 159 } 160 161 func parseElementBorderOnCropbox(s string, nup *model.NUp) error { 162 // w 163 // w r g b 164 // w #c 165 // w round 166 // w round r g b 167 // w round #c 168 169 var err error 170 171 b := strings.Split(s, " ") 172 if len(b) == 0 || len(b) > 5 { 173 return errors.Errorf("pdfcpu: borders: need 1,2,3,4 or 5 int values, %s\n", s) 174 } 175 176 switch b[0] { 177 case "off", "false", "f": 178 return nil 179 case "on", "true", "t": 180 nup.BorderOnCropbox = &model.BorderStyling{Width: 1} 181 return nil 182 } 183 184 nup.BorderOnCropbox = &model.BorderStyling{} 185 width, err := strconv.ParseFloat(b[0], 64) 186 if err != nil { 187 return err 188 } 189 if width == 0 { 190 return errors.New("pdfcpu: borders: need width > 0") 191 } 192 nup.BorderOnCropbox.Width = width 193 194 if len(b) == 1 { 195 return nil 196 } 197 if strings.HasPrefix("round", b[1]) { 198 style := types.LJRound 199 nup.BorderOnCropbox.LineStyle = &style 200 if len(b) == 2 { 201 return nil 202 } 203 c, err := color.ParseColor(strings.Join(b[2:], " ")) 204 nup.BorderOnCropbox.Color = &c 205 return err 206 } 207 208 c, err := color.ParseColor(strings.Join(b[1:], " ")) 209 nup.BorderOnCropbox.Color = &c 210 return err 211 } 212 213 func parseBookletGuides(s string, nup *model.NUp) error { 214 switch strings.ToLower(s) { 215 case "on", "true", "t": 216 nup.BookletGuides = true 217 case "off", "false", "f": 218 nup.BookletGuides = false 219 default: 220 return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false t/f") 221 } 222 223 return nil 224 } 225 226 func parseBookletMultifolio(s string, nup *model.NUp) error { 227 switch strings.ToLower(s) { 228 case "on", "true", "t": 229 nup.MultiFolio = true 230 case "off", "false", "f": 231 nup.MultiFolio = false 232 default: 233 return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false t/f") 234 } 235 236 return nil 237 } 238 239 func parseBookletFolioSize(s string, nup *model.NUp) error { 240 i, err := strconv.Atoi(s) 241 if err != nil { 242 return errors.Errorf("pdfcpu: illegal folio size: must be an numeric value, %s\n", s) 243 } 244 245 nup.FolioSize = i 246 return nil 247 } 248 249 func parseBookletType(s string, nup *model.NUp) error { 250 switch strings.ToLower(s) { 251 case "booklet": 252 nup.BookletType = model.Booklet 253 case "bookletadvanced": 254 nup.BookletType = model.BookletAdvanced 255 case "perfectbound": 256 nup.BookletType = model.BookletPerfectBound 257 default: 258 return errors.New("pdfcpu: booklet type, please provide one of: booklet perfectbound") 259 } 260 return nil 261 } 262 263 func parseBookletBinding(s string, nup *model.NUp) error { 264 switch strings.ToLower(s) { 265 case "short": 266 nup.BookletBinding = model.ShortEdge 267 case "long": 268 nup.BookletBinding = model.LongEdge 269 default: 270 return errors.New("pdfcpu: booklet binding, please provide one of: short long") 271 } 272 return nil 273 } 274 275 func parseElementMargin(s string, nup *model.NUp) error { 276 f, err := strconv.ParseFloat(s, 64) 277 if err != nil { 278 return err 279 } 280 281 if f < 0 { 282 return errors.New("pdfcpu: nUp margin, Please provide a positive value") 283 } 284 285 nup.Margin = types.ToUserSpace(f, nup.InpUnit) 286 287 return nil 288 } 289 290 func parseSheetBackgroundColor(s string, nup *model.NUp) error { 291 c, err := color.ParseColor(s) 292 if err != nil { 293 return err 294 } 295 nup.BgColor = &c 296 return nil 297 } 298 299 // ParseNUpDetails parses a NUp command string into an internal structure. 300 func ParseNUpDetails(s string, nup *model.NUp) error { 301 if s == "" { 302 return errInvalidNUpConfig 303 } 304 305 ss := strings.Split(s, ",") 306 307 for _, s := range ss { 308 309 ss1 := strings.Split(s, ":") 310 if len(ss1) != 2 { 311 return errInvalidNUpConfig 312 } 313 314 paramPrefix := strings.TrimSpace(ss1[0]) 315 paramValueStr := strings.TrimSpace(ss1[1]) 316 317 if err := nupParamMap.Handle(paramPrefix, paramValueStr, nup); err != nil { 318 return err 319 } 320 } 321 322 return nil 323 } 324 325 // PDFNUpConfig returns an NUp configuration for Nup-ing PDF files. 326 func PDFNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { 327 nup := model.DefaultNUpConfig() 328 if conf == nil { 329 conf = model.NewDefaultConfiguration() 330 } 331 nup.InpUnit = conf.Unit 332 if desc != "" { 333 if err := ParseNUpDetails(desc, nup); err != nil { 334 return nil, err 335 } 336 } 337 if !types.IntMemberOf(val, NUpValues) { 338 ss := make([]string, len(NUpValues)) 339 for i, v := range NUpValues { 340 ss[i] = strconv.Itoa(v) 341 } 342 return nil, errors.Errorf("pdfcpu: n must be one of %s", strings.Join(ss, ", ")) 343 } 344 return nup, ParseNUpValue(val, nup) 345 } 346 347 // ImageNUpConfig returns an NUp configuration for Nup-ing image files. 348 func ImageNUpConfig(val int, desc string, conf *model.Configuration) (*model.NUp, error) { 349 nup, err := PDFNUpConfig(val, desc, conf) 350 if err != nil { 351 return nil, err 352 } 353 nup.ImgInputFile = true 354 return nup, nil 355 } 356 357 // PDFGridConfig returns a grid configuration for Nup-ing PDF files. 358 func PDFGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { 359 nup := model.DefaultNUpConfig() 360 if conf == nil { 361 conf = model.NewDefaultConfiguration() 362 } 363 nup.InpUnit = conf.Unit 364 nup.PageGrid = true 365 if desc != "" { 366 if err := ParseNUpDetails(desc, nup); err != nil { 367 return nil, err 368 } 369 } 370 return nup, ParseNUpGridDefinition(rows, cols, nup) 371 } 372 373 // ImageGridConfig returns a grid configuration for Nup-ing image files. 374 func ImageGridConfig(rows, cols int, desc string, conf *model.Configuration) (*model.NUp, error) { 375 nup, err := PDFGridConfig(rows, cols, desc, conf) 376 if err != nil { 377 return nil, err 378 } 379 nup.ImgInputFile = true 380 return nup, nil 381 } 382 383 // ParseNUpValue parses the NUp value into an internal structure. 384 func ParseNUpValue(n int, nUp *model.NUp) error { 385 // The n-Up layout depends on the orientation of the chosen output paper size. 386 // This optional paper size may also be specified by dimensions in user unit. 387 // The default paper size is A4 or A4P (A4 in portrait mode) respectively. 388 var portrait bool 389 if nUp.PageDim == nil { 390 portrait = types.PaperSize[nUp.PageSize].Portrait() 391 } else { 392 portrait = types.RectForDim(nUp.PageDim.Width, nUp.PageDim.Height).Portrait() 393 } 394 395 d := nUpDims[n] 396 if portrait { 397 d.Width, d.Height = d.Height, d.Width 398 } 399 400 nUp.Grid = &d 401 402 return nil 403 } 404 405 // ParseNUpGridDefinition parses NUp grid dimensions into an internal structure. 406 func ParseNUpGridDefinition(rows, cols int, nUp *model.NUp) error { 407 m := cols 408 if m <= 0 { 409 return errInvalidGridDims 410 } 411 412 n := rows 413 if n <= 0 { 414 return errInvalidGridDims 415 } 416 417 nUp.Grid = &types.Dim{Width: float64(m), Height: float64(n)} 418 419 return nil 420 } 421 422 func nUpImagePDFBytes(w io.Writer, imgWidth, imgHeight int, nup *model.NUp, formResID string) { 423 for _, r := range nup.RectsForGrid() { 424 // Append to content stream. 425 model.NUpTilePDFBytes(w, types.RectForDim(float64(imgWidth), float64(imgHeight)), r, formResID, nup, false) 426 } 427 } 428 429 func createNUpFormForImage(xRefTable *model.XRefTable, imgIndRef *types.IndirectRef, w, h, i int) (*types.IndirectRef, error) { 430 imgResID := fmt.Sprintf("Im%d", i) 431 bb := types.RectForDim(float64(w), float64(h)) 432 433 var b bytes.Buffer 434 fmt.Fprintf(&b, "/%s Do ", imgResID) 435 436 d := types.Dict( 437 map[string]types.Object{ 438 "ProcSet": types.NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"), 439 "XObject": types.Dict(map[string]types.Object{imgResID: *imgIndRef}), 440 }, 441 ) 442 443 ir, err := xRefTable.IndRefForNewObject(d) 444 if err != nil { 445 return nil, err 446 } 447 448 sd := types.StreamDict{ 449 Dict: types.Dict( 450 map[string]types.Object{ 451 "Type": types.Name("XObject"), 452 "Subtype": types.Name("Form"), 453 "BBox": bb.Array(), 454 "Matrix": types.NewIntegerArray(1, 0, 0, 1, 0, 0), 455 "Resources": *ir, 456 }, 457 ), 458 Content: b.Bytes(), 459 FilterPipeline: []types.PDFFilter{{Name: filter.Flate, DecodeParms: nil}}, 460 } 461 462 sd.InsertName("Filter", filter.Flate) 463 464 if err = sd.Encode(); err != nil { 465 return nil, err 466 } 467 468 return xRefTable.IndRefForNewObject(sd) 469 } 470 471 // NewNUpPageForImage creates a new page dict in xRefTable for given image filename and n-up conf. 472 func NewNUpPageForImage(xRefTable *model.XRefTable, fileName string, parentIndRef *types.IndirectRef, nup *model.NUp) (*types.IndirectRef, error) { 473 f, err := os.Open(fileName) 474 if err != nil { 475 return nil, err 476 } 477 defer f.Close() 478 479 // create image dict. 480 imgIndRef, w, h, err := model.CreateImageResource(xRefTable, f) 481 if err != nil { 482 return nil, err 483 } 484 485 resID := 0 486 487 formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, resID) 488 if err != nil { 489 return nil, err 490 } 491 492 formResID := fmt.Sprintf("Fm%d", resID) 493 494 resourceDict := types.Dict( 495 map[string]types.Object{ 496 "XObject": types.Dict(map[string]types.Object{formResID: *formIndRef}), 497 }, 498 ) 499 500 resIndRef, err := xRefTable.IndRefForNewObject(resourceDict) 501 if err != nil { 502 return nil, err 503 } 504 505 var buf bytes.Buffer 506 nUpImagePDFBytes(&buf, w, h, nup, formResID) 507 sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes()) 508 if err = sd.Encode(); err != nil { 509 return nil, err 510 } 511 512 contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) 513 if err != nil { 514 return nil, err 515 } 516 517 dim := nup.PageDim 518 mediaBox := types.RectForDim(dim.Width, dim.Height) 519 520 pageDict := types.Dict( 521 map[string]types.Object{ 522 "Type": types.Name("Page"), 523 "Parent": *parentIndRef, 524 "MediaBox": mediaBox.Array(), 525 "Resources": *resIndRef, 526 "Contents": *contentsIndRef, 527 }, 528 ) 529 530 return xRefTable.IndRefForNewObject(pageDict) 531 } 532 533 // NUpFromOneImage creates one page with instances of one image. 534 func NUpFromOneImage(ctx *model.Context, fileName string, nup *model.NUp, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { 535 indRef, err := NewNUpPageForImage(ctx.XRefTable, fileName, pagesIndRef, nup) 536 if err != nil { 537 return err 538 } 539 540 if err := ctx.SetValid(*indRef); err != nil { 541 return err 542 } 543 544 if err = model.AppendPageTree(indRef, 1, pagesDict); err != nil { 545 return err 546 } 547 548 ctx.PageCount++ 549 550 return nil 551 } 552 553 func wrapUpPage(ctx *model.Context, nup *model.NUp, d types.Dict, buf bytes.Buffer, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { 554 xRefTable := ctx.XRefTable 555 556 var fm model.FontMap 557 if nup.BookletGuides { 558 // For booklets only. 559 fm = model.DrawBookletGuides(nup, &buf) 560 } 561 562 resourceDict := types.Dict( 563 map[string]types.Object{ 564 "XObject": d, 565 }, 566 ) 567 568 fontRes, err := pdffont.FontResources(xRefTable, fm) 569 if err != nil { 570 return err 571 } 572 573 if len(fontRes) > 0 { 574 resourceDict["Font"] = fontRes 575 } 576 577 resIndRef, err := xRefTable.IndRefForNewObject(resourceDict) 578 if err != nil { 579 return err 580 } 581 582 sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes()) 583 if err = sd.Encode(); err != nil { 584 return err 585 } 586 587 contentsIndRef, err := xRefTable.IndRefForNewObject(*sd) 588 if err != nil { 589 return err 590 } 591 592 dim := nup.PageDim 593 mediaBox := types.RectForDim(dim.Width, dim.Height) 594 595 pageDict := types.Dict( 596 map[string]types.Object{ 597 "Type": types.Name("Page"), 598 "Parent": *pagesIndRef, 599 "MediaBox": mediaBox.Array(), 600 "Resources": *resIndRef, 601 "Contents": *contentsIndRef, 602 }, 603 ) 604 605 indRef, err := xRefTable.IndRefForNewObject(pageDict) 606 if err != nil { 607 return err 608 } 609 610 if err := ctx.SetValid(*indRef); err != nil { 611 return err 612 } 613 614 if err = model.AppendPageTree(indRef, 1, pagesDict); err != nil { 615 return err 616 } 617 618 ctx.PageCount++ 619 620 return nil 621 } 622 623 func nupPageNumber(i int, sortedPageNumbers []int) int { 624 var pageNumber int 625 if i < len(sortedPageNumbers) { 626 pageNumber = sortedPageNumbers[i] 627 } 628 return pageNumber 629 } 630 631 func sortSelectedPages(pages types.IntSet) []int { 632 var pageNumbers []int 633 for k, v := range pages { 634 if v { 635 pageNumbers = append(pageNumbers, k) 636 } 637 } 638 sort.Ints(pageNumbers) 639 return pageNumbers 640 } 641 642 func nupPages( 643 ctx *model.Context, 644 selectedPages types.IntSet, 645 nup *model.NUp, 646 pagesDict types.Dict, 647 pagesIndRef *types.IndirectRef) error { 648 649 var buf bytes.Buffer 650 formsResDict := types.NewDict() 651 rr := nup.RectsForGrid() 652 653 sortedPageNumbers := sortSelectedPages(selectedPages) 654 pageCount := len(sortedPageNumbers) 655 // pageCount must be a multiple of n. 656 // If not, we will insert blank pages at the end. 657 if pageCount%nup.N() != 0 { 658 pageCount += nup.N() - pageCount%nup.N() 659 } 660 661 for i := 0; i < pageCount; i++ { 662 663 if i > 0 && i%len(rr) == 0 { 664 // Wrap complete page. 665 if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { 666 return err 667 } 668 buf.Reset() 669 formsResDict = types.NewDict() 670 } 671 672 rDest := rr[i%len(rr)] 673 674 pageNr := nupPageNumber(i, sortedPageNumbers) 675 if pageNr == 0 { 676 // This is an empty page at the end. 677 if nup.BgColor != nil { 678 draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) 679 } 680 continue 681 } 682 683 if err := ctx.NUpTilePDFBytesForPDF(pageNr, formsResDict, &buf, rDest, nup, false); err != nil { 684 return err 685 } 686 } 687 688 // Wrap incomplete nUp page. 689 return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) 690 } 691 692 // NUpFromMultipleImages creates pages in NUp-style rendering each image once. 693 func NUpFromMultipleImages(ctx *model.Context, fileNames []string, nup *model.NUp, pagesDict types.Dict, pagesIndRef *types.IndirectRef) error { 694 if nup.PageGrid { 695 nup.PageDim.Width *= nup.Grid.Width 696 nup.PageDim.Height *= nup.Grid.Height 697 } 698 699 xRefTable := ctx.XRefTable 700 formsResDict := types.NewDict() 701 var buf bytes.Buffer 702 rr := nup.RectsForGrid() 703 704 // fileCount must be a multiple of n. 705 // If not, we will insert blank pages at the end. 706 fileCount := len(fileNames) 707 if fileCount%nup.N() != 0 { 708 fileCount += nup.N() - fileCount%nup.N() 709 } 710 711 for i := 0; i < fileCount; i++ { 712 713 if i > 0 && i%len(rr) == 0 { 714 // Wrap complete nUp page. 715 if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil { 716 return err 717 } 718 buf.Reset() 719 formsResDict = types.NewDict() 720 } 721 722 rDest := rr[i%len(rr)] 723 724 var fileName string 725 if i < len(fileNames) { 726 fileName = fileNames[i] 727 } 728 729 if fileName == "" { 730 // This is an empty page at the end. 731 if nup.BgColor != nil { 732 draw.FillRectNoBorder(&buf, rDest, *nup.BgColor) 733 } 734 continue 735 } 736 737 f, err := os.Open(fileName) 738 if err != nil { 739 return err 740 } 741 742 imgIndRef, w, h, err := model.CreateImageResource(xRefTable, f) 743 if err != nil { 744 return err 745 } 746 747 if err := f.Close(); err != nil { 748 return err 749 } 750 751 formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, i) 752 if err != nil { 753 return err 754 } 755 756 formResID := fmt.Sprintf("Fm%d", i) 757 formsResDict.Insert(formResID, *formIndRef) 758 759 // Append to content stream of page i. 760 model.NUpTilePDFBytes(&buf, types.RectForDim(float64(w), float64(h)), rr[i%len(rr)], formResID, nup, false) 761 } 762 763 // Wrap incomplete nUp page. 764 return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef) 765 } 766 767 // NUpFromPDF creates an n-up version of the PDF represented by xRefTable. 768 func NUpFromPDF(ctx *model.Context, selectedPages types.IntSet, nup *model.NUp) error { 769 var mb *types.Rectangle 770 if nup.PageDim == nil { 771 // No page dimensions specified, use cropBox of page 1 as mediaBox(=cropBox). 772 consolidateRes := false 773 d, _, inhPAttrs, err := ctx.PageDict(1, consolidateRes) 774 if err != nil { 775 return err 776 } 777 if d == nil { 778 return errors.Errorf("unknown page number: %d\n", 1) 779 } 780 781 cropBox := inhPAttrs.MediaBox 782 if inhPAttrs.CropBox != nil { 783 cropBox = inhPAttrs.CropBox 784 } 785 786 // Account for existing rotation. 787 if inhPAttrs.Rotate != 0 { 788 if types.IntMemberOf(inhPAttrs.Rotate, []int{+90, -90, +270, -270}) { 789 w := cropBox.Width() 790 cropBox.UR.X = cropBox.LL.X + cropBox.Height() 791 cropBox.UR.Y = cropBox.LL.Y + w 792 } 793 } 794 795 mb = cropBox 796 } else { 797 mb = types.RectForDim(nup.PageDim.Width, nup.PageDim.Height) 798 } 799 800 if nup.PageGrid { 801 mb.UR.X = mb.LL.X + float64(nup.Grid.Width)*mb.Width() 802 mb.UR.Y = mb.LL.Y + float64(nup.Grid.Height)*mb.Height() 803 } 804 805 pagesDict := types.Dict( 806 map[string]types.Object{ 807 "Type": types.Name("Pages"), 808 "Count": types.Integer(0), 809 "MediaBox": mb.Array(), 810 }, 811 ) 812 813 pagesIndRef, err := ctx.IndRefForNewObject(pagesDict) 814 if err != nil { 815 return err 816 } 817 818 nup.PageDim = &types.Dim{Width: mb.Width(), Height: mb.Height()} 819 820 if err = nupPages(ctx, selectedPages, nup, pagesDict, pagesIndRef); err != nil { 821 return err 822 } 823 824 // Replace original pagesDict. 825 rootDict, err := ctx.Catalog() 826 if err != nil { 827 return err 828 } 829 830 rootDict.Update("Pages", *pagesIndRef) 831 832 return nil 833 }