muun-recovery/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/nup.go

996 lines
23 KiB
Go

/*
Copyright 2018 The pdfcpu Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pdfcpu
import (
"bytes"
"fmt"
"io"
"math"
"os"
"sort"
"strconv"
"strings"
"github.com/pdfcpu/pdfcpu/pkg/filter"
"github.com/pkg/errors"
)
var (
errInvalidGridID = errors.New("pdfcpu nup: n must be one of 2, 3, 4, 6, 8, 9, 12, 16")
errInvalidGridDims = errors.New("pdfcpu grid: dimensions: m >= 0, n >= 0")
errInvalidNUpConfig = errors.New("pdfcpu: invalid configuration string")
)
var (
nUpValues = []int{2, 3, 4, 6, 8, 9, 12, 16}
nUpDims = map[int]Dim{
2: {2, 1},
3: {3, 1},
4: {2, 2},
6: {3, 2},
8: {4, 2},
9: {3, 3},
12: {4, 3},
16: {4, 4},
}
)
type nUpParamMap map[string]func(string, *NUp) error
var nupParamMap = nUpParamMap{
"dimensions": parseDimensionsNUp,
"formsize": parsePageFormatNUp,
"papersize": parsePageFormatNUp,
"orientation": parseOrientation,
"border": parseElementBorder,
"margin": parseElementMargin,
"backgroundcolor": parseSheetBackgroundColor,
"bgcolor": parseSheetBackgroundColor,
"guides": parseBookletGuides,
}
// Handle applies parameter completion and if successful
// parses the parameter values into import.
func (m nUpParamMap) Handle(paramPrefix, paramValueStr string, nup *NUp) error {
var param string
// Completion support
for k := range m {
if !strings.HasPrefix(k, paramPrefix) {
continue
}
if len(param) > 0 {
return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix)
}
param = k
}
if param == "" {
return errors.Errorf("pdfcpu: ambiguous parameter prefix \"%s\"", paramPrefix)
}
return m[param](paramValueStr, nup)
}
// NUp represents the command details for the command "NUp".
type NUp struct {
PageDim *Dim // Page dimensions in display unit.
PageSize string // Paper size eg. A4L, A4P, A4(=default=A4P), see paperSize.go
UserDim bool // true if one of dimensions or paperSize provided overriding the default.
Orient orientation // One of rd(=default),dr,ld,dl
Grid *Dim // Intra page grid dimensions eg (2,2)
PageGrid bool // Create a mxn grid of pages for PDF inputfiles only (think "extra page n-Up").
ImgInputFile bool // Process image or PDF input files.
Margin int // Cropbox for n-Up content.
Border bool // Draw bounding box.
BookletGuides bool // Draw folding and cutting lines
InpUnit DisplayUnit // input display unit.
BgColor *SimpleColor // background color
}
// DefaultNUpConfig returns the default NUp configuration.
func DefaultNUpConfig() *NUp {
return &NUp{
PageSize: "A4",
Orient: RightDown,
Margin: 3,
Border: true,
}
}
func (nup NUp) String() string {
return fmt.Sprintf("N-Up conf: %s %s, orient=%s, grid=%s, pageGrid=%t, isImage=%t\n",
nup.PageSize, *nup.PageDim, nup.Orient, *nup.Grid, nup.PageGrid, nup.ImgInputFile)
}
// N returns the nUp value.
func (nup NUp) N() int {
return int(nup.Grid.Height * nup.Grid.Width)
}
type orientation int
func (o orientation) String() string {
switch o {
case RightDown:
return "right down"
case DownRight:
return "down right"
case LeftDown:
return "left down"
case DownLeft:
return "down left"
}
return ""
}
// These are the defined anchors for relative positioning.
const (
RightDown orientation = iota
DownRight
LeftDown
DownLeft
)
func parsePageFormatNUp(s string, nup *NUp) (err error) {
if nup.UserDim {
return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed")
}
nup.PageDim, nup.PageSize, err = parsePageFormat(s)
nup.UserDim = true
return err
}
func parseDimensionsNUp(s string, nup *NUp) (err error) {
if nup.UserDim {
return errors.New("pdfcpu: only one of formsize(papersize) or dimensions allowed")
}
nup.PageDim, nup.PageSize, err = parsePageDim(s, nup.InpUnit)
nup.UserDim = true
return err
}
func parseOrientation(s string, nup *NUp) error {
switch s {
case "rd":
nup.Orient = RightDown
case "dr":
nup.Orient = DownRight
case "ld":
nup.Orient = LeftDown
case "dl":
nup.Orient = DownLeft
default:
return errors.Errorf("pdfcpu: unknown nUp orientation: %s", s)
}
return nil
}
func parseElementBorder(s string, nup *NUp) error {
switch strings.ToLower(s) {
case "on", "true":
nup.Border = true
case "off", "false":
nup.Border = false
default:
return errors.New("pdfcpu: nUp border, please provide one of: on/off true/false")
}
return nil
}
func parseBookletGuides(s string, nup *NUp) error {
switch strings.ToLower(s) {
case "on", "true":
nup.BookletGuides = true
case "off", "false":
nup.BookletGuides = false
default:
return errors.New("pdfcpu: booklet guides, please provide one of: on/off true/false")
}
return nil
}
func parseElementMargin(s string, nup *NUp) error {
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
if f < 0 {
return errors.New("pdfcpu: nUp margin, Please provide a positive value")
}
nup.Margin = int(toUserSpace(f, nup.InpUnit))
return nil
}
func parseSheetBackgroundColor(s string, nup *NUp) error {
c, err := parseColor(s)
if err != nil {
return err
}
nup.BgColor = &c
return nil
}
// ParseNUpDetails parses a NUp command string into an internal structure.
func ParseNUpDetails(s string, nup *NUp) error {
if s == "" {
return errInvalidNUpConfig
}
ss := strings.Split(s, ",")
for _, s := range ss {
ss1 := strings.Split(s, ":")
if len(ss1) != 2 {
return errInvalidNUpConfig
}
paramPrefix := strings.TrimSpace(ss1[0])
paramValueStr := strings.TrimSpace(ss1[1])
if err := nupParamMap.Handle(paramPrefix, paramValueStr, nup); err != nil {
return err
}
}
return nil
}
// PDFNUpConfig returns an NUp configuration for Nup-ing PDF files.
func PDFNUpConfig(val int, desc string) (*NUp, error) {
nup := DefaultNUpConfig()
if desc != "" {
if err := ParseNUpDetails(desc, nup); err != nil {
return nil, err
}
}
return nup, ParseNUpValue(val, nup)
}
// ImageNUpConfig returns an NUp configuration for Nup-ing image files.
func ImageNUpConfig(val int, desc string) (*NUp, error) {
nup, err := PDFNUpConfig(val, desc)
if err != nil {
return nil, err
}
nup.ImgInputFile = true
return nup, nil
}
// PDFGridConfig returns a grid configuration for Nup-ing PDF files.
func PDFGridConfig(rows, cols int, desc string) (*NUp, error) {
nup := DefaultNUpConfig()
nup.PageGrid = true
if desc != "" {
if err := ParseNUpDetails(desc, nup); err != nil {
return nil, err
}
}
return nup, ParseNUpGridDefinition(rows, cols, nup)
}
// ImageGridConfig returns a grid configuration for Nup-ing image files.
func ImageGridConfig(rows, cols int, desc string) (*NUp, error) {
nup, err := PDFGridConfig(rows, cols, desc)
if err != nil {
return nil, err
}
nup.ImgInputFile = true
return nup, nil
}
// ParseNUpValue parses the NUp value into an internal structure.
func ParseNUpValue(n int, nUp *NUp) error {
if !IntMemberOf(n, nUpValues) {
return errInvalidGridID
}
// The n-Up layout depends on the orientation of the chosen output paper size.
// This optional paper size may also be specified by dimensions in user unit.
// The default paper size is A4 or A4P (A4 in portrait mode) respectively.
var portrait bool
if nUp.PageDim == nil {
portrait = PaperSize[nUp.PageSize].Portrait()
} else {
portrait = RectForDim(nUp.PageDim.Width, nUp.PageDim.Height).Portrait()
}
d := nUpDims[n]
if portrait {
d.Width, d.Height = d.Height, d.Width
}
nUp.Grid = &d
return nil
}
// ParseNUpGridDefinition parses NUp grid dimensions into an internal structure.
func ParseNUpGridDefinition(rows, cols int, nUp *NUp) error {
m := cols
if m <= 0 {
return errInvalidGridDims
}
n := rows
if m <= 0 {
return errInvalidGridDims
}
nUp.Grid = &Dim{float64(m), float64(n)}
return nil
}
func rectsForGrid(nup *NUp) []*Rectangle {
cols := int(nup.Grid.Width)
rows := int(nup.Grid.Height)
maxX := float64(nup.PageDim.Width)
maxY := float64(nup.PageDim.Height)
gw := maxX / float64(cols)
gh := maxY / float64(rows)
var llx, lly float64
rr := []*Rectangle{}
switch nup.Orient {
case RightDown:
for i := rows - 1; i >= 0; i-- {
for j := 0; j < cols; j++ {
llx = float64(j) * gw
lly = float64(i) * gh
rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
}
}
case DownRight:
for i := 0; i < cols; i++ {
for j := rows - 1; j >= 0; j-- {
llx = float64(i) * gw
lly = float64(j) * gh
rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
}
}
case LeftDown:
for i := rows - 1; i >= 0; i-- {
for j := cols - 1; j >= 0; j-- {
llx = float64(j) * gw
lly = float64(i) * gh
rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
}
}
case DownLeft:
for i := cols - 1; i >= 0; i-- {
for j := rows - 1; j >= 0; j-- {
llx = float64(i) * gw
lly = float64(j) * gh
rr = append(rr, Rect(llx, lly, llx+gw, lly+gh))
}
}
}
return rr
}
func bestFitRectIntoRect(rSrc, rDest *Rectangle) (w, h, dx, dy, rot float64) {
if rSrc.FitsWithin(rDest) {
// Translate rSrc into center of rDest without scaling.
w = rSrc.Width()
h = rSrc.Height()
dx = rDest.Width()/2 - rSrc.Width()/2
dy = rDest.Height()/2 - rSrc.Height()/2
return
}
if rSrc.Landscape() {
if rDest.Landscape() {
if rSrc.AspectRatio() > rDest.AspectRatio() {
w = rDest.Width()
h = rSrc.ScaledHeight(w)
dy = (rDest.Height() - h) / 2
} else {
h = rDest.Height()
w = rSrc.ScaledWidth(h)
dx = (rDest.Width() - w) / 2
}
} else {
rot = 90
if 1/rSrc.AspectRatio() < rDest.AspectRatio() {
w = rDest.Height()
h = rSrc.ScaledHeight(w)
dx = (rDest.Width() - h) / 2
} else {
h = rDest.Width()
w = rSrc.ScaledWidth(h)
dy = (rDest.Height() - w) / 2
}
}
return
}
if rSrc.Portrait() {
if rDest.Portrait() {
if rSrc.AspectRatio() < rDest.AspectRatio() {
h = rDest.Height()
w = rSrc.ScaledWidth(h)
dx = (rDest.Width() - w) / 2
} else {
w = rDest.Width()
h = rSrc.ScaledHeight(w)
dy = (rDest.Height() - h) / 2
}
} else {
rot = 90
if 1/rSrc.AspectRatio() > rDest.AspectRatio() {
h = rDest.Width()
w = rSrc.ScaledWidth(h)
dy = (rDest.Height() - w) / 2
} else {
w = rDest.Height()
h = rSrc.ScaledHeight(w)
dx = (rDest.Width() - h) / 2
}
}
return
}
w = rDest.Height()
if rDest.Portrait() {
w = rDest.Width()
}
h = w
dx = rDest.Width()/2 - rSrc.Width()/2
dy = rDest.Height()/2 - rSrc.Height()/2
return
}
func nUpTilePDFBytes(wr io.Writer, rSrc, rDest *Rectangle, formResID string, nup *NUp, rotate bool) {
// rScr is a rectangular region represented by form formResID in form space.
// rDest is an arbitrary rectangular region in dest space.
// It is the location where we want the form content to get rendered on a "best fit" basis.
// Accounting for the aspect ratios of rSrc and rDest "best fit" tries to fit the largest version of rScr into rDest.
// This may result in a 90 degree rotation.
// rotate indicates if we need to apply a post rotation of 180 degrees eg for booklets.
// Draw bounding box.
if nup.Border {
fmt.Fprintf(wr, "[]0 d 0.1 w %.2f %.2f m %.2f %.2f l %.2f %.2f l %.2f %.2f l s ",
rDest.LL.X, rDest.LL.Y, rDest.UR.X, rDest.LL.Y, rDest.UR.X, rDest.UR.Y, rDest.LL.X, rDest.UR.Y,
)
}
// Apply margin to rDest which potentially makes it smaller.
rDestCr := rDest.CroppedCopy(float64(nup.Margin))
// Calculate transform matrix.
// Best fit translation of a source rectangle into a destination rectangle.
w, h, dx, dy, r := bestFitRectIntoRect(rSrc, rDestCr)
if nup.BgColor != nil {
if nup.ImgInputFile {
// Fill background.
FillRectStacked(wr, rDest, *nup.BgColor)
} else if nup.Margin > 0 {
// Fill margins.
m := float64(nup.Margin)
drawMargins(wr, *nup.BgColor, rDest, 0, m, m, m, m)
}
}
// Apply additional rotation.
if rotate {
r += 180
}
sx := w
sy := h
if !nup.ImgInputFile {
sx /= rSrc.Width()
sy /= rSrc.Height()
}
sin := math.Sin(r * float64(degToRad))
cos := math.Cos(r * float64(degToRad))
switch r {
case 90:
dx += h
case 180:
dx += w
dy += h
case 270:
dy += w
}
dx += rDestCr.LL.X
dy += rDestCr.LL.Y
m := calcTransformMatrix(sx, sy, sin, cos, dx, dy)
// Apply transform matrix and display form.
fmt.Fprintf(wr, "q %.2f %.2f %.2f %.2f %.2f %.2f cm /%s Do Q ",
m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1], formResID)
}
func nUpImagePDFBytes(w io.Writer, imgWidth, imgHeight int, nup *NUp, formResID string) {
for _, r := range rectsForGrid(nup) {
// Append to content stream.
nUpTilePDFBytes(w, RectForDim(float64(imgWidth), float64(imgHeight)), r, formResID, nup, false)
}
}
func createNUpFormForImage(xRefTable *XRefTable, imgIndRef *IndirectRef, w, h, i int) (*IndirectRef, error) {
imgResID := fmt.Sprintf("Im%d", i)
bb := RectForDim(float64(w), float64(h))
var b bytes.Buffer
fmt.Fprintf(&b, "/%s Do ", imgResID)
d := Dict(
map[string]Object{
"ProcSet": NewNameArray("PDF", "Text", "ImageB", "ImageC", "ImageI"),
"XObject": Dict(map[string]Object{imgResID: *imgIndRef}),
},
)
ir, err := xRefTable.IndRefForNewObject(d)
if err != nil {
return nil, err
}
sd := StreamDict{
Dict: Dict(
map[string]Object{
"Type": Name("XObject"),
"Subtype": Name("Form"),
"BBox": bb.Array(),
"Matrix": NewIntegerArray(1, 0, 0, 1, 0, 0),
"Resources": *ir,
},
),
Content: b.Bytes(),
FilterPipeline: []PDFFilter{{Name: filter.Flate, DecodeParms: nil}},
}
sd.InsertName("Filter", filter.Flate)
if err = sd.Encode(); err != nil {
return nil, err
}
return xRefTable.IndRefForNewObject(sd)
}
func createNUpFormForPDF(xRefTable *XRefTable, resDict *IndirectRef, content []byte, cropBox *Rectangle) (*IndirectRef, error) {
sd := StreamDict{
Dict: Dict(
map[string]Object{
"Type": Name("XObject"),
"Subtype": Name("Form"),
"BBox": cropBox.Array(),
"Matrix": NewNumberArray(1, 0, 0, 1, -cropBox.LL.X, -cropBox.LL.Y),
"Resources": *resDict,
},
),
Content: content,
FilterPipeline: []PDFFilter{{Name: filter.Flate, DecodeParms: nil}},
}
sd.InsertName("Filter", filter.Flate)
if err := sd.Encode(); err != nil {
return nil, err
}
return xRefTable.IndRefForNewObject(sd)
}
// NewNUpPageForImage creates a new page dict in xRefTable for given image filename and n-up conf.
func NewNUpPageForImage(xRefTable *XRefTable, fileName string, parentIndRef *IndirectRef, nup *NUp) (*IndirectRef, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer f.Close()
// create image dict.
imgIndRef, w, h, err := createImageResource(xRefTable, f)
if err != nil {
return nil, err
}
resID := 0
formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, resID)
if err != nil {
return nil, err
}
formResID := fmt.Sprintf("Fm%d", resID)
resourceDict := Dict(
map[string]Object{
"XObject": Dict(map[string]Object{formResID: *formIndRef}),
},
)
resIndRef, err := xRefTable.IndRefForNewObject(resourceDict)
if err != nil {
return nil, err
}
var buf bytes.Buffer
nUpImagePDFBytes(&buf, w, h, nup, formResID)
sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes())
if err = sd.Encode(); err != nil {
return nil, err
}
contentsIndRef, err := xRefTable.IndRefForNewObject(*sd)
if err != nil {
return nil, err
}
dim := nup.PageDim
mediaBox := RectForDim(dim.Width, dim.Height)
pageDict := Dict(
map[string]Object{
"Type": Name("Page"),
"Parent": *parentIndRef,
"MediaBox": mediaBox.Array(),
"Resources": *resIndRef,
"Contents": *contentsIndRef,
},
)
return xRefTable.IndRefForNewObject(pageDict)
}
// NUpFromOneImage creates one page with instances of one image.
func NUpFromOneImage(ctx *Context, fileName string, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
indRef, err := NewNUpPageForImage(ctx.XRefTable, fileName, pagesIndRef, nup)
if err != nil {
return err
}
if err = AppendPageTree(indRef, 1, pagesDict); err != nil {
return err
}
ctx.PageCount++
return nil
}
func wrapUpPage(ctx *Context, nup *NUp, d Dict, buf bytes.Buffer, pagesDict Dict, pagesIndRef *IndirectRef) error {
xRefTable := ctx.XRefTable
var fm FontMap
if nup.BookletGuides {
// For booklets only.
fm = drawBookletGuides(nup, &buf)
}
resourceDict := Dict(
map[string]Object{
"XObject": d,
},
)
fontRes, err := fontResources(xRefTable, fm)
if err != nil {
return err
}
if len(fontRes) > 0 {
resourceDict["Font"] = fontRes
}
resIndRef, err := xRefTable.IndRefForNewObject(resourceDict)
if err != nil {
return err
}
sd, _ := xRefTable.NewStreamDictForBuf(buf.Bytes())
if err = sd.Encode(); err != nil {
return err
}
contentsIndRef, err := xRefTable.IndRefForNewObject(*sd)
if err != nil {
return err
}
dim := nup.PageDim
mediaBox := RectForDim(dim.Width, dim.Height)
pageDict := Dict(
map[string]Object{
"Type": Name("Page"),
"Parent": *pagesIndRef,
"MediaBox": mediaBox.Array(),
"Resources": *resIndRef,
"Contents": *contentsIndRef,
},
)
indRef, err := xRefTable.IndRefForNewObject(pageDict)
if err != nil {
return err
}
if err = AppendPageTree(indRef, 1, pagesDict); err != nil {
return err
}
ctx.PageCount++
return nil
}
// NUpFromMultipleImages creates pages in NUp-style rendering each image once.
func NUpFromMultipleImages(ctx *Context, fileNames []string, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
if nup.PageGrid {
nup.PageDim.Width *= nup.Grid.Width
nup.PageDim.Height *= nup.Grid.Height
}
xRefTable := ctx.XRefTable
formsResDict := NewDict()
var buf bytes.Buffer
rr := rectsForGrid(nup)
// fileCount must be a multiple of n.
// If not, we will insert blank pages at the end.
fileCount := len(fileNames)
if fileCount%nup.N() != 0 {
fileCount += nup.N() - fileCount%nup.N()
}
for i := 0; i < fileCount; i++ {
if i > 0 && i%len(rr) == 0 {
// Wrap complete nUp page.
if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil {
return err
}
buf.Reset()
formsResDict = NewDict()
}
rDest := rr[i%len(rr)]
var fileName string
if i < len(fileNames) {
fileName = fileNames[i]
}
if fileName == "" {
// This is an empty page at the end.
if nup.BgColor != nil {
FillRectStacked(&buf, rDest, *nup.BgColor)
}
continue
}
f, err := os.Open(fileName)
if err != nil {
return err
}
imgIndRef, w, h, err := createImageResource(xRefTable, f)
if err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
formIndRef, err := createNUpFormForImage(xRefTable, imgIndRef, w, h, i)
if err != nil {
return err
}
formResID := fmt.Sprintf("Fm%d", i)
formsResDict.Insert(formResID, *formIndRef)
// Append to content stream of page i.
nUpTilePDFBytes(&buf, RectForDim(float64(w), float64(h)), rr[i%len(rr)], formResID, nup, false)
}
// Wrap incomplete nUp page.
return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef)
}
func sortSelectedPages(pages IntSet) []int {
var pageNumbers []int
for k, v := range pages {
if v {
pageNumbers = append(pageNumbers, k)
}
}
sort.Ints(pageNumbers)
return pageNumbers
}
func nupPageNumber(i int, sortedPageNumbers []int) int {
var pageNumber int
if i < len(sortedPageNumbers) {
pageNumber = sortedPageNumbers[i]
}
return pageNumber
}
func (ctx *Context) nupPages(selectedPages IntSet, nup *NUp, pagesDict Dict, pagesIndRef *IndirectRef) error {
var buf bytes.Buffer
xRefTable := ctx.XRefTable
formsResDict := NewDict()
rr := rectsForGrid(nup)
sortedPageNumbers := sortSelectedPages(selectedPages)
pageCount := len(sortedPageNumbers)
// pageCount must be a multiple of n.
// If not, we will insert blank pages at the end.
if pageCount%nup.N() != 0 {
pageCount += nup.N() - pageCount%nup.N()
}
for i := 0; i < pageCount; i++ {
if i > 0 && i%len(rr) == 0 {
// Wrap complete nUp page.
if err := wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef); err != nil {
return err
}
buf.Reset()
formsResDict = NewDict()
}
rDest := rr[i%len(rr)]
pageNumber := nupPageNumber(i, sortedPageNumbers)
if pageNumber == 0 {
// This is an empty page at the end of a booklet.
if nup.BgColor != nil {
FillRectStacked(&buf, rDest, *nup.BgColor)
}
continue
}
consolidateRes := true
d, inhPAttrs, err := ctx.PageDict(pageNumber, consolidateRes)
if err != nil {
return err
}
if d == nil {
return errors.Errorf("pdfcpu: unknown page number: %d\n", i)
}
// Retrieve content stream bytes.
bb, err := xRefTable.PageContent(d)
if err == errNoContent {
continue
}
if err != nil {
return err
}
// Create an object for this resDict in xRefTable.
ir, err := ctx.IndRefForNewObject(inhPAttrs.resources)
if err != nil {
return err
}
cropBox := inhPAttrs.mediaBox
if inhPAttrs.cropBox != nil {
cropBox = inhPAttrs.cropBox
}
formIndRef, err := createNUpFormForPDF(xRefTable, ir, bb, cropBox)
if err != nil {
return err
}
formResID := fmt.Sprintf("Fm%d", i)
formsResDict.Insert(formResID, *formIndRef)
// Append to content stream of page i.
nUpTilePDFBytes(&buf, cropBox, rDest, formResID, nup, false)
}
// Wrap incomplete nUp page.
return wrapUpPage(ctx, nup, formsResDict, buf, pagesDict, pagesIndRef)
}
// NUpFromPDF creates an n-up version of the PDF represented by xRefTable.
func (ctx *Context) NUpFromPDF(selectedPages IntSet, nup *NUp) error {
var mb *Rectangle
if nup.PageDim == nil {
// No page dimensions specified, use mediaBox of page 1.
consolidateRes := false
d, inhPAttrs, err := ctx.PageDict(1, consolidateRes)
if err != nil {
return err
}
if d == nil {
return errors.Errorf("unknown page number: %d\n", 1)
}
mb = inhPAttrs.mediaBox
} else {
mb = RectForDim(nup.PageDim.Width, nup.PageDim.Height)
}
if nup.PageGrid {
mb.UR.X = mb.LL.X + float64(nup.Grid.Width)*mb.Width()
mb.UR.Y = mb.LL.Y + float64(nup.Grid.Height)*mb.Height()
}
pagesDict := Dict(
map[string]Object{
"Type": Name("Pages"),
"Count": Integer(0),
"MediaBox": mb.Array(),
},
)
pagesIndRef, err := ctx.IndRefForNewObject(pagesDict)
if err != nil {
return err
}
nup.PageDim = &Dim{mb.Width(), mb.Height()}
if err = ctx.nupPages(selectedPages, nup, pagesDict, pagesIndRef); err != nil {
return err
}
// Replace original pagesDict.
rootDict, err := ctx.Catalog()
if err != nil {
return err
}
rootDict.Update("Pages", *pagesIndRef)
return nil
}