3
pdftools.pdfposter - scale and tile PDF images/pages to print on multiple pages.
6
# Copyright 2008-2013 by Hartmut Goebel <h.goebel@crazy-compilers.com>
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
# General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
__author__ = "Hartmut Goebel <h.goebel@crazy-compilers.com>"
23
__copyright__ = "Copyright 2008-2013 by Hartmut Goebel <h.goebel@crazy-compilers.com>"
24
__licence__ = "GNU General Public License version 3 (GPL v3)"
27
# ignore some warnings for pyPDF < 1.13
29
warnings.filterwarnings('ignore', "the sets module is deprecated")
30
warnings.filterwarnings('ignore', "the md5 module is deprecated")
32
from pyPdf.pdf import PdfFileWriter, PdfFileReader, PageObject, getRectangle, \
33
ArrayObject, ContentStream, NameObject, FloatObject, RectangleObject
36
from logging import log
39
DEFAULT_MEDIASIZE = 'a4'
47
'ft' : (864, 864), # 12 inch
49
'cm' : (10 *mm, 10 *mm),
50
'meter':(1000* mm, 1000* mm),
52
# American page sizes (taken from psposter.c)
53
"monarch" : (279, 540),
54
"statement": (396, 612),
55
"executive": (540, 720),
56
"quarto" : (610, 780),
57
"letter" : (612, 792),
59
"legal" : (612, 1008),
60
"tabloid" : (792, 1224),
61
"ledger" : (792, 1224),
63
# ISO page sizes (taken from psposter.c)
76
"dinlang" : (281, 595), # 1/3 a4
77
"envdinlang": (312, 624), # envelobe for din-lang
95
# Japanese page sizes (taken from psposter.c)
108
"comm10": (298, 684),
109
"com10" : (298, 684),
110
"env10" : (298, 684),
113
class DecryptionError(ValueError): pass
116
PAGE_BOXES = ("/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox")
118
def rectangle2box(pdfbox):
120
'width' : pdfbox.getUpperRight_x()-pdfbox.getLowerLeft_x(),
121
'height' : pdfbox.getUpperRight_y()-pdfbox.getLowerLeft_y(),
122
'offset_x': pdfbox.getLowerLeft_x(),
123
'offset_y': pdfbox.getLowerLeft_y(),
124
# the following are unused, but need to be set to make
125
# `rotate_box()` work
133
('offset_x', 'offset_y'),
134
('units_x', 'units_y')):
135
box[a], box[b] = box[b], box[a]
137
def rotate2portrait(box, which):
138
'if box is landscape spec, rotate to portrait'
139
if ( box['width' ]-box['offset_x']
140
> box['height']-box['offset_y']):
142
log(18, 'Rotating %s specs to portrait format', which)
145
def decide_num_pages(inbox, mediabox, posterbox, scale=None):
146
"""decide on number of pages"""
147
# avoid changing original posterbox when handling multiple pages
148
# (if --scale, posterbox is None)
149
posterbox = posterbox and posterbox.copy()
150
cutmargin = {'x': 0, 'y': 0} # todo
151
whitemargin = {'x': 0, 'y': 0} # todo
152
# media and image sizes (inbox) are fixed already
153
# available drawing area per sheet
154
drawable_x = mediabox['width' ] - 2*cutmargin['x']
155
drawable_y = mediabox['height'] - 2*cutmargin['y']
159
inbox_x = float(inbox['width' ])
160
inbox_y = float(inbox['height'])
161
log(17, 'input dimensions: %.2f %.2f (trimbox of input page)',
165
# user did not specify scale factor, calculate from output size
166
# todo: fix assuming posterbox offset = 0,0
167
log(17, 'output dimensions: %.2f %.2f (poster size)',
168
posterbox['width'], posterbox['height'])
170
# ensure poster spec are portrait
171
if rotate2portrait(posterbox, 'poster'):
172
rotate = rotate != True # xor
174
# if the input page has landscape format rotate the
175
# poster spec to landscape, too
176
if inbox_x > inbox_y:
177
log(18, 'Rotating poster specs since input page is landscape')
178
rotate = rotate != True # xor
179
rotate_box(posterbox)
180
log(18, 'rotated output dimensions: %.2f %.2f (poster size)',
181
posterbox['width'], posterbox['height'])
183
scale = min(posterbox['width' ] / inbox_x,
184
posterbox['height'] / inbox_y)
185
log(18, 'Calculated page scaling factor: %f', scale)
187
# use round() to avoid floating point roundup errors
188
size_x = round(inbox_x*scale - whitemargin['x'], 4)
189
size_y = round(inbox_y*scale - whitemargin['y'], 4)
190
log(17, 'output dimensions: %.2f %.2f (calculated)', size_x, size_y)
192
# num pages without rotation
193
nx0 = int(math.ceil( size_x / drawable_x))
194
ny0 = int(math.ceil( size_y / drawable_y))
195
# num pages with rotation
196
nx1 = int(math.ceil( size_x / drawable_y))
197
ny1 = int(math.ceil( size_y / drawable_x))
199
log(17, 'Pages w/o rotation %s x %s' , nx0, ny0)
200
log(17, 'Pages w/ rotation %s x %s' , nx1, ny1)
202
# Decide for rotation to get the minimum page count.
203
# (Rotation is considered as media versus input page, which is
204
# totally independent of the portrait or landscape style of the
206
rotate = (rotate and (nx0*ny0) == (nx1*ny1)) or (nx0*ny0) > (nx1*ny1)
207
log(17, 'Decided for rotation: %s', rotate and 'yes' or 'no')
216
log(19, "Deciding for %d column%s and %d row%s of %s pages.",
217
ncols, (ncols==1) and "s" or "",
218
nrows, (nrows==1) and "s" or "",
219
rotate and "landscape" or "portrait")
220
return ncols, nrows, scale, rotate
224
from pyPdf.pdf import RectangleObject, NameObject
225
newpage = PageObject(page.pdf)
227
# Copy Rectangles to be manipulatable
228
for attr in PAGE_BOXES:
229
if page.has_key(attr):
230
newpage[NameObject(attr)] = RectangleObject(list(page[attr]))
233
def _clip_pdf_page(page, x, y, width, height):
234
content = ContentStream(page["/Contents"].getObject(), page.pdf)
235
content.operations[:0] = [
236
([], 'q'), # save graphic state
237
([], 'n'), # cancel path w/ filling or stroking
238
(RectangleObject((x, y, width, height)), 're'), # rectangle path
241
content.operations.append([[], "Q"]) # restore graphic state
242
page[NameObject('/Contents')] = content
245
def _scale_pdf_page(page, factor):
246
for boxname in PAGE_BOXES:
247
# skip if real box does not exits (avoid fallback to other boxes)
248
if not page.get(boxname):
250
box = getRectangle(page, boxname, None)
251
box.lowerLeft = [float(i) * factor for i in box.lowerLeft ]
252
box.upperRight = [float(i) * factor for i in box.upperRight]
253
#print boxname, type(box), box
254
# put transformation matrix in front of page content
255
content = ContentStream(page["/Contents"].getObject(), page.pdf)
256
content.operations.insert(0, [[], '%f 0 0 %f 0 0 cm' %(factor,factor)] )
257
page[NameObject('/Contents')] = content
260
def posterize(outpdf, page, mediabox, posterbox, scale, use_ArtBox=False):
263
mediabox : size secs of the media to print on
264
posterbox: size secs of the resulting poster
265
scale: scale factor (to be used instead of posterbox)
268
inbox = rectangle2box(page.artBox)
270
inbox = rectangle2box(page.trimBox)
271
_clip_pdf_page(page, inbox['offset_x'], inbox['offset_y'],
272
inbox['width'], inbox['height'])
273
ncols, nrows, scale, rotate = decide_num_pages(inbox, mediabox,
275
mediabox = mediabox.copy()
276
_scale_pdf_page(page, scale)
278
page.rotateClockwise(90)
281
# area to put on each page (allows for overlay of margin)
282
h_step = mediabox['width'] - mediabox['offset_x']
283
v_step = mediabox['height'] - mediabox['offset_y']
286
trimbox = rectangle2box(page.artBox)
288
trimbox = rectangle2box(page.trimBox)
289
h_pos = float(trimbox['offset_x'])
290
h_max, v_max = float(trimbox['width']), float(trimbox['height'])
291
for col in range(ncols):
292
v_pos = float(trimbox['offset_y']) + (nrows-1) * v_step
293
for row in range(nrows):
294
log(17, 'Creating page with offset: %.2f %.2f' % (h_pos, v_pos))
295
newpage = copyPage(page)
296
# todo: if remaining area is smaller than mediaBox, add a
297
# transparent fill box behind, so the real content is in
298
# the lower left corner
299
newpage.mediaBox = RectangleObject((h_pos, v_pos,
302
newpage.trimBox = RectangleObject((h_pos, v_pos,
303
min(h_max, h_pos + h_step),
304
min(v_max, v_pos + v_step)))
305
newpage.artBox = newpage.trimBox
306
outpdf.addPage(newpage)
312
return getpass.getpass()
314
def main(opts, infilename, outfilename, password_hook=password_hook):
315
logging.basicConfig(level=20-opts.verbose, format="%(message)s")
316
outpdf = PdfFileWriter()
317
inpdf = PdfFileReader(open(infilename, 'rb'))
319
if inpdf.isEncrypted:
320
log(16, 'File is encrypted')
321
# try empty password first
322
if not inpdf.decrypt(''):
323
if not inpdf.decrypt(password_hook()):
324
raise DecryptionError("Can't decrypt PDF. Wrong Password?")
326
log(18, 'Mediasize : %(units_x)sx%(units_y)s %(unit)s' % opts.media_size)
327
log(17, ' %(width).2f %(height).2f dots' % opts.media_size)
329
log(18, 'Scaling by: %f' % opts.scale)
331
log(18, 'Postersize: %(units_x)sx%(units_y)s %(unit)s' % opts.poster_size)
332
log(17, ' %(width).2f %(height).2f dots' % opts.poster_size)
334
for i, page in enumerate(inpdf.pages):
335
log(19, '---- processing page %i -----', i+1)
336
posterize(outpdf, page, opts.media_size, opts.poster_size, opts.scale,
339
outpdf.write(open(outfilename, 'wb'))