#!/usr/bin/python # vim:ts=8:sw=4:expandtab:encoding=utf-8 '''img4pdf: Embed image files into a PDF file. Copyright hashao 2008 GNU GPL version 3 or later: # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. Depends on the reportlab (python-reportlab on Debian) and python-image packages. ''' __version__ = '0.2' import sys import os import math try: import Image except ImportError: print '*** Error: Please install python-image package.' sys.exit() try: from reportlab.pdfgen import canvas from reportlab.lib import pagesizes import reportlab.lib.colors except ImportError: print '*** Error: Please install python-reportlab package.' sys.exit() PAGESIZES = ['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'B0', 'B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'ELEVENSEVENTEEN', 'LEGAL', 'LETTER'] LINESPACE = 12 LABELMARGIN = 12 import locale locale.setlocale(locale.LC_CTYPE) CHARSET = locale.getpreferredencoding() def get_max_size(files): '''Find the max width and hight of a list of images.''' wmax = 0 hmax = 0 for f in files: try: img = Image.open(f) except: continue w, h = img.size wmax = max(wmax, w) hmax = max(hmax, h) return wmax, hmax class Writer: def __init__(self): self.do_tag = True self.first_page = None self.page_size = pagesizes.A4 self.max_level = 0 self.outfile = None self.canvas = None self.fontfile = None self.fontname = None self.font_size = 12 self.font_color = reportlab.lib.colors.toColor('green') self.path_level = 0 self.landscape = False self.center = False self.title = None def set_page_size(self, size): self.page_size = size def set_first_page(self, first_page): '''Set the given page as the page1.''' self.first_page = first_page def set_do_tag(self, do_tag): '''Label images with pathname.''' self.do_tag = do_tag def set_landscape(self, landscape): self.landscape = landscape if landscape: self.page_size = reportlab.lib.pagesizes.landscape(self.page_size) else: self.page_size = reportlab.lib.pagesizes.portrait(self.page_size) def set_title(self, title): if title: self.title = title.decode(CHARSET, 'replace').encode('utf-8') def setup_canvas(self, outfile): cv = canvas.Canvas(outfile, pagesize = self.page_size, verbosity=1) cv.setAuthor('Generated by img4pdf (GNU GPL 3.0 or later).') if self.title: cv.setTitle(self.title) self.outfile = outfile self.canvas = cv if self.fontname: cv.setFont(self.fontname, self.font_size) def set_font_size(self, size): if size: self.font_size = size def set_font_color(self, color): if color: if ',' in color: color = [float(x) for x in color.split(',')] if max(color) > 1 or min(color) < 0: print '*** Error: Color value must be 0 <= n <= 1.' sys.exit(1) self.font_color = reportlab.lib.colors.toColor(color) def set_font(self, fontfile): '''Set a user TTF font for text. Hackish!''' if not self.do_tag: return import reportlab.pdfbase.ttfonts import reportlab.pdfbase.pdfmetrics fontface = reportlab.pdfbase.ttfonts.TTFontFace(fontfile) self.fontfile = fontfile self.fontname = fontface.name font = reportlab.pdfbase.ttfonts.TTFont(self.fontname, fontfile) reportlab.pdfbase.pdfmetrics.registerFont(font) if self.canvas: self.canvas.setFont(self.fontname, self.font_size) def count_max_level(self, total): '''Get max level for the bookmarks.''' max_level = int(math.log(total, 10)) if total < 2*10**max_level and total > 1: max_level -= 1 self.max_level = max_level def add_level(self, cv, idx, hl, level): '''Add a extra levels to max from the give level at the idx.''' max_level = self.max_level for i in range(level, max_level): fmt = '%%0%dd' % (max_level+1,) txt = fmt % (10**i*idx/10**i,) cv.bookmarkHorizontalAbsolute(txt, hl) cv.addOutlineEntry(txt, txt, level=i, closed= (i < max_level/2)) def fill_level(self, cv, idx, hl): '''Add extra level to the numbered page.''' if not idx % 10: current_level = idx and int(math.log(idx, 10)) digit = current_level if idx == 0: digit = self.max_level while idx % (10**digit): digit -= 1 self.add_level(cv, idx, hl, self.max_level-digit) return def fix_path_level(self, path): '''Cut the path to the user set level.''' if self.path_level != 0: parts = path.split(os.sep) if not parts[0]: del parts[0] path = os.sep.join(parts[self.path_level:]) return path def push_images(self, cv, images): '''Save images to a canvas.''' wo, ho = self.page_size # original size wl, hl = wo, ho # unfilled width and height cx, xy = 0, 0 page_count = 0 for im_id in range(len(images)): im = images[im_id] im_u8 = im.decode(CHARSET, 'replace').encode('utf-8') print 'writing %s...' % im try: pim = Image.open(im) except IOError: print sys.exc_info()[1] continue wi, hi = pim.size # Try to fit big image to one page. if wi > wo or hi > ho: if wi/float(wo) > hi/float(ho): wreal = wo hreal = wreal*hi/float(wi) else: hreal = ho wreal = hreal*wi/float(hi) else: wreal = wi hreal = hi # Check if we have enough space left in the page. if hreal > hl: cv.showPage() hl = ho # Set book mark if not self.first_page: mark = im_u8 mark = self.fix_path_level(mark) elif page_count < 1: if im == self.first_page: self.count_max_level(len(images) - im_id) self.fill_level(cv, page_count, hl) page_count = 1 mark = 'Page_%d' % page_count page_count += 1 else: mark = im_u8 mark = self.fix_path_level(mark) else: self.fill_level(cv, page_count, hl) mark = 'Page_%d' % page_count page_count += 1 cv.bookmarkHorizontalAbsolute(im_u8, hl) cv.addOutlineEntry(mark, im_u8, level=self.max_level) x0 = 0 if not self.center else (wo-wreal)/2 if wreal == wi and hreal == hi: w, h = cv.drawImage(im, x0, hl - hreal) else: w, h = cv.drawImage(im, x0, hl - hreal, wreal, hreal, preserveAspectRatio=1) # Label image if self.do_tag: im_label = im_u8 im_label = self.fix_path_level(im_label) cv.saveState() cv.setFillColor(self.font_color) cv.drawString(x0+LABELMARGIN, hl-hreal+LABELMARGIN, im_label) cv.restoreState() hl -= hreal + LINESPACE # update space left cv.showPage() def save(self): print "saving %s..." % (self.outfile,) self.canvas.save() def usage(): text = [ '%prog [options] image1 image2 image3...\n\n', 'If there are too many files, double quote the patterns, ', 'E.g. "pictures/*.png".', ] return ''.join(text) def do_args(): '''Parse command line arguments. return: options, filenames, parser.''' import optparse parser = optparse.OptionParser(version='%prog version ' + __version__, description='img4pdf: Embed multiple image files into a PDF file.', usage=usage()) parser.add_option('-f', '--first-page', action='store', help="The image filename for the first page.") parser.add_option('--font', action='store', type='string', help="Font (TTF file) for image label.") parser.add_option('--font-size', action='store', type='int', help="Font size for image label.") parser.add_option('--font-color', action='store', type='string', help=('Font color for image label. For possible named colors, ' + 'use "--font-color help". Also "#FFFFFF" or C,M,Y,K or R,G,B.')) parser.add_option('--write-no-tag', action='store_false', help="Don't write the filename on a image.") parser.add_option('-p', '--paper-size', action='store', type='string', help="Paper Size in [%s]. Default: " "largest width and height of all images." % (', '.join(PAGESIZES),)) parser.add_option('--center', action='store_true', help="Put the images at the center of pages.") parser.add_option('--landscape', action='store_true', help="Paper orientation set to landscape. Default: portrait.") parser.add_option('--path-level', action='store', type='int', help="Use level of path for the image label. Can be negative. " "Default: 0 for full path.") parser.add_option('-t', '--title', action='store', help="Title for the PDF file.") parser.add_option('-o', '--output', action='store', help="Output image file name.") parser.set_defaults(write_no_tag=True, path_level=0, landscape=False, center=False, font_color='green') options, args = parser.parse_args() # print options return (options, args, parser) def main(): options, args, parser = do_args() outfile = options.output or 'out.pdf' if options.font_color == 'help': import textwrap text = ', '.join(reportlab.lib.colors.getAllNamedColors().keys()) wrapper = textwrap.TextWrapper() wrapper.initial_indent = wrapper.subsequent_indent = ' '*4 print 'Color Names:' print wrapper.fill(text) return if len(args) <= 0: parser.print_help() return infiles = [] import glob for arg in args: if '*' in arg: txt = os.path.expandvars(os.path.expanduser(arg)) infiles.extend(sorted(glob.glob(txt))) else: infiles.append(arg) writer = Writer() if options.paper_size: p = options.paper_size.upper() if p in PAGESIZES: psize = getattr(pagesizes, p) else: import re pat = re.compile(r'^((\d|\.)+)[^0-9.]+((\d|\.)+)$') g = pat.match(p.strip()) if g: psize = g.group(1), g.group(3) else: psize = get_max_size(infiles) if options.first_page and options.first_page not in infiles: print ('*** Warning: The first page (%s) is not in the input files.' % options.first_page) writer.set_page_size(psize) # only effect if page is set. options.paper_size and writer.set_landscape(options.landscape) writer.set_first_page(options.first_page) writer.set_do_tag(options.write_no_tag) writer.path_level = options.path_level writer.center = options.center writer.set_title(options.title) writer.setup_canvas(outfile) writer.set_font_size(options.font_size) writer.set_font_color(options.font_color) options.font and writer.set_font(options.font) writer.push_images(writer.canvas, infiles) writer.save() if __name__ == '__main__': main()