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  }