#!/usr/bin/env python
__libname__ = "pdfgen"
__version__ = "1.0"
__copyright__ = "(C) 2009 by Martin J. Fiedler <martin.fiedler@gmx.net>"
__license__ = """
This software is published under the terms of KeyJ's Research License,
version 0.2. Usage of this software is subject to the following conditions:
0. There's no warranty whatsoever. The author(s) of this software can not
   be held liable for any damages that occur when using this software.
1. This software may be used freely for both non-commercial and commercial
   purposes.
2. This software may be redistributed freely as long as no fees are charged
   for the distribution and this license information is included.
3. This software may be modified freely except for this license information,
   which must not be changed in any way.
4. If anything other than configuration, indentation or comments have been
   altered in the code, the original author(s) must receive a copy of the
   modified code.
"""
__doc__ = """ PDF generator library and image-to-PDF converter.

For information on command-line use, run this script with the '-h'
option. Note that the command line program doesn't offer all the options
present in the Python library, like selectable smoothing, enforcement or
prevention of JPEG compression and direct access to PDF internals.

LIBRARY USAGE EXAMPLE:
    pdf = PDFFile("output.pdf", pagesize=(210,297), title="Example")
    pdf["Keywords"] = "pdfgen, example"

    logo = PDFImage(pdf, "logo.png", jpeg=True, quality=95)
    img1 = PDFImage(pdf, "example1.jpg")
    img2 = PDFImage(pdf, "example2.tif", dpi=300)

    page1 = PDFPage(pdf)
    page2 = PDFPage(pdf)

    logo.put(page1, pos=(15,15), size=(30,30))
    logo.put(page2, pos=(15,15), size=(30,30))
    img1.put(page1, size=200, rotate=1)
    img2.put(page2)

    pdf.close()
"""
__help__ = """Usage: $prog [<OPTIONS>] <INPUT1> [[<OPTIONS>] <INPUT2> ...]
Generate PDF files from images.

PROGRAM OPTIONS
    -h          show this help text
    -v          enable verbose operation

OUTPUT OPTIONS
    -o <FILE>   set output filename (default: 'pdfgen_output.pdf' or the name
                of the input file with '.pdf' if there is only one input file)
    -f <X>x<Y>  set page size in millimeters (default: A4 = 210x297)
    -m <K>=<X>  set file metadata. valid keys (K):
                    title, author, subject, keywords, creator
    -p          create a new page. If this option is not used, each input image
                will be placed on a page of its own.

INPUT FILE OPTIONS
The following options affect the next input image only:
  Image resolution:
    -d 0        auto-detect image resolution from the input file (default)
    -d <DPI>    set image resolution in dots per inch
    -d <X>x<Y>  set image resolution for X and Y direction independently
  Image rotation:
    -r <DEG>    rotate image by DEG degrees clockwise. Only multiples of 90
                degrees are allowed. (default: 0 = no rotation)
  Image position (after rotation):
    -c          center the image on the page (default)
    -x <X>,<Y>  explicitly set the position of the upper-left corner of the
                image, relative to the upper-left corner or the page,
                in millimeters
  Image size (after rotation):
    -s 0        auto-detect image size from the image resolution (default)
    -s <MM>     set image size so that the longest side is as large as the
                specified value, in millimeters
    -s <X>x<Y>  scale image to the specified size, regardless of aspect ratio
    -s <X>x0    scale image to the specified width
    -s 0x<Y>    scale image to the specified height

GLOBAL INPUT FILE OPTIONS
    -D <...>    like -d, but affects all following images
    -R <...>    like -r, but affects all following images
    -C          like -c, but affects all following images
    -X <...>    like -x, but affects all following images
    -S <...>    like -s, but affects all following images

All options except -h, -v, -o, -f and -m are positional.

Images will generally be left unmodified, unless they use a color space other
than monochrome (1-bit), 8-bit grayscale, 8-bit RGB or 8-bit CMYK, in which
case they will be converted to RGB first. Images will be stored in the PDF file
in a losslessly-compressed manner, except for JPEG files, which will be used
verbatim (i.e. with the same JPEG compression as the original file).

EXAMPLES
  $prog image.jpg
    - convert image.jpg to image.pdf

  $prog -o output.pdf -m title="My Image" -f 216x279 -d 300 -r 90 input.tif
    - convert input.tif to output.pdf; set the document title to "My Image",
      use Letter page format, assume the input image has a resolution of 300
      dpi, and rotate the image by 90 degrees clockwise

  $prog -p -x 10,10 a.jpg -x 120,10 b.jpg -p c.jpg
    - generate pdfgen_output.pdf with three images: a.jpg and b.jpg on page 1,
      next to each other, and c.jpg on page 2, centered
"""
__all__ = "mm2pt PDFError PDFFile PDFObj PDFRef PDFDict PDFInfoKeyAllowed PDFInfoDict PDFStream PDFPage PDFImage".split()
import sys, types, os, time, zlib, cStringIO, Image, JpegImagePlugin

_mm2pt = 72.0 / 25.4
def mm2pt(mm):
    "Convert millimeters to PostScript points."
    return mm * _mm2pt

def _is_class(haystack, needle):
    if haystack == needle:
        return True
    for base in haystack.__bases__:
        if _is_class(base, needle):
            return True
    return False

class PDFError(RuntimeError):
    def __init__(self, msg):
        RuntimeError.__init__(self)
        self.strerror = msg

class PDFFile(object):
    """ PDF file writer object. """

    def __init__(self, f, pagesize=(210,297), **info):
        """ Constructor.
        - f: output file (either a file name or a file-like object)
        - pagesize: page size as a (x,y) tuple (in millimeters);
                    default: A4 (210 x 296 mm)
        - **info: file information dictionary. valid fields:
                  Title, Author, Subject, Keywords, Creator
        """
        self.open = False
        self.pagesize = pagesize
        if type(f) in (types.StringType, types.UnicodeType):
            self.f = open(f, "wb")
        else:
            self.f = f
        self.write = self.f.write
        self.tell = self.f.tell
        self.objs = {}
        self.current_obj = None
        self.write("%PDF-1.4\n")
        self.root = PDFObj(self)
        self.pagetree = PDFObj(self)
        self.info = PDFInfoDict(self, **info)
        self.root.write("<<\n/Type /Catalog\n/Pages %s\n>>\n" % self.pagetree.ref())
        self.pagetree.write("<<\n/Type /Pages\n/MediaBox [ 0 0 %d %d ]\n/Kids [ " % \
                            (int(mm2pt(pagesize[0]) + 0.5), int(mm2pt(pagesize[1]) + 0.5)))
        self.info['Producer'] = "%s %s" % (__libname__, __version__)
        if not "CreationDate" in self.info.contents:
            tz = time.timezone / 60
            if tz < 0:
                tz = -tz
                tz = "+%02d'%02d'" % (tz / 60, tz % 60)
            elif tz > 0:
                tz = "-%02d'%02d'" % (tz / 60, tz % 60)
            else:
                tz = "Z"
            self.info['CreationDate'] = time.strftime("D:%Y%m%d%H%M%S") + tz
        self.pages = []
        self.open = True

    def _add_object(self, obj):
        if self.objs:
            num = max(self.objs.iterkeys()) + 1
        else:
            num = 1
        self.objs[num] = obj
        return num

    def __getitem__(self, key):
        return self.info[key]
    def __setitem__(self, key, value):
        self.info[key] = value
    def __delitem__(self, key):
        del self.info[key]

    def close(self):
        """Finish and close the PDF file."""
        if not self.open:
            return

        # finish page tree
        self.pagetree.write(" ".join([str(page.ref()) for page in self.pages]))
        self.pagetree.write(" ]\n/Count %d\n>>\n" % len(self.pages))

        # flush and close all objects
        if self.current_obj:
            self.current_obj.close()
        for obj in self.objs.itervalues():
            if not obj.offset:
                obj.flush()
            if obj.open:
                obj.close()

        # generate the xref table
        startxref = self.tell()
        self.write("xref\n")
        objs = self.objs.keys() + [0]
        objs.sort()
        while objs:
            base = objs[0]
            n = 1
            while (n < len(objs)) and (objs[n] == (base + n)):
                n += 1
            self.write("%d %d\n" % (base, n))
            for obj in objs[:n]:
                if obj:
                    self.write("%010d 00000 n \n" % self.objs[obj].offset)
                else:
                    self.write("0000000000 65535 f \n")
            del objs[:n]

        # generate the trailer
        self.write("trailer\n<<\n/Size %d\n/Root %s\n/Info %s\n>>\n" % (max(self.objs.iterkeys()) + 1, self.root.ref(), self.info.ref()))

        # generate startxref and close file
        self.write("startxref\n%d\n%%%%EOF\n" % startxref)
        self.f.close()
        self.open = False

    def __del__(self):
        self.close()

class PDFObj(object):
    """ PDF indirect object. """

    def __init__(self, parent_file):
        """ Constructor.
        - parent_file: the PDF file this object belongs to
        """
        self.f = parent_file
        self.num = self.f._add_object(self)
        self.f.objs[self.num] = self
        self.data = "%d 0 obj\n" % self.num
        self.offset = 0
        self.open = True

    def write(self, data):
        """ Append data to the object body. """
        if not self.open:
            raise PDFError("attempt to write into closed object")
        if self.f.current_obj == self:
            self.f.write(data)
        else:
            self.data += data

    def close(self):
        """ Finish the object. """
        if not self.open:
            return
        self.write("endobj\n")
        self.open = False
        if self.f.current_obj == self:
            self.f.current_obj = None

    def flush(self):
        """ Flush the object to disk.
        When this method is called, all unwritten data of this object is
        written into the PDF file and all further write() calls will also
        write directly into the file. Only one object can be flushed at any
        time for any PDF file; if flush() is called on object X while
        object Y is flushed, object Y will be finished (closed) first.
        This method is useful for writing large bulks of data like images:
        calling flush() before write() forces the writes to go directly to
        disk instead of buffering them in memory first.
        """
        last_obj = self.f.current_obj
        if last_obj == self:
            return
        if last_obj:
            last_obj.close()
        self.f.current_obj = self
        self.offset = self.f.tell()
        self.f.write(self.data)
        self.data = None

    def ref(self):
        """ Return a PDF reference to this object. """
        return PDFRef(self)

class PDFRef(object):
    """ PDF indirect object reference. """

    def __init__(self, obj):
        """ Constructor.
        - obj: The PDF indirect object this reference shall refer to.
        """
        self.obj = obj.num

    def ref(self):
        """ Return a PDF reference.
        This is a convenience method that makes it possible to call ref()
        on object references, not only the objects themselves.
        """
        return self

    def __str__(self):
        """ Return the PDF string representation of this reference. """
        return "%d 0 R" % self.obj

class PDFDict(PDFObj):
    """ PDF indirect object containing a dictionary.
    This class provides convenient generation of PDF dictionaries. Values
    can either be added in the constructor or by using this object like a
    normal Python dictionary. Keys are automatically prefixed with a slash
    to form PDF name objects. The dictionary object will not be generated
    until the object is closed.
    """

    def __init__(self, parent_file, **kwargs):
        """ Constructor.
        - parent_file: the PDF file this object belongs to
        - **kwargs: initial values of the dictionary
        """
        PDFObj.__init__(self, parent_file)
        self.contents = {}
        for k, v in kwargs.iteritems():
            self[k] = v
        self.written = False

    def __getitem__(self, key):
        return self.contents[self._nicekey(key)]

    def __setitem__(self, key, value):
        key =self._nicekey(key)
        if not self._is_allowed(key):
            raise PDFError("key '%s' not allowed here" % key)
        self.contents[key] = value

    def __delitem__(self, key):
        del self.contents[self._nicekey(key)]

    def write_dict(self):
        """ Write the dictionary to disk or the internal buffer of PDFObj.
        This method is called automatically by close().
        """
        if self.written:
            return
        self.write("<<\n")
        klist = dict([(key, None) for key in self.contents])
        sklist = []
        for pref in ("Type", "Subtype", "Length", "Width", "Height"):
            if pref in klist:
                sklist.append(pref)
                del klist[pref]
        sklist.extend(klist.iterkeys())
        for key in sklist:
            self.write("/%s %s\n" % (key, self._stringify(self.contents[key])))
        self.write(">>\n")
        self.written = True

    def _nicekey(self, key):
        if key[:1] == '/':
            key = key[1:]
        return key

    def _stringify(self, value):
        return str(value)

    def _is_allowed(self, key):
        return True

    def close(self):
        self.write_dict()
        PDFObj.close(self)

def PDFInfoKeyAllowed(key):
    "Determines whether a certain key is valid for PDF file information."
    key = key[0].upper() + key[1:]
    return key in ("Title", "Author", "Subject", "Keywords", "Creator", "Producer", "CreationDate", "ModDate")

class PDFInfoDict(PDFDict):
    """ Class that wraps some of the specialties of PDF info dictionaries. """

    def _nicekey(self, key):
        if key[:1] == '/':
            key = key[1:]
        return key[0].upper() + key[1:]

    def _is_allowed(self, key):
        return PDFInfoKeyAllowed(key)

    def _stringify(self, value):
        value = str(value)
        value = value.replace("\\", "\\\\")
        value = value.replace("(", "\\(")
        value = value.replace(")", "\\)")
        return "(%s)" % value

class PDFStream(PDFDict):
    """ PDF stream object.
    Automatically generates a PDF stream object with the accompanying
    stream dictionary. If the 'compress' property is set to True, the
    contents of the stream will be automatically compressed using the
    zlib algorithm (/FlateDecode in PDF).
    """

    def __init__(self, parent_file, **kwargs):
        """ Constructor.
        - parent_file: the PDF file this stream belongs to
        - **kwargs: initial values of the stream dictionary
        """
        PDFDict.__init__(self, parent_file, **kwargs)
        self.compress = self.contents.get('compress', False)
        if self.compress:
            del self.contents['compress']
        if not("Length" in self.contents):
            self.contents["Length"] = _pdf_stream_length(self.f, self).ref()
        self.in_dict = False
        self.in_stream = False
        self.length = 0

    def write(self, data):
        """ Write stream data.
        This method will generate and write the stream dictionary before
        starting to write actual data. This means that all values of the
        stream dictionary must be set BEFORE write() is called for the
        first time!
        """
        if self.in_dict:
            return PDFObj.write(self, data)
        if not self.in_stream:
            if self.compress:
                f = self.contents.get("Filter", "").strip()
                if not f:
                    f = "/FlateDecode"
                elif f.endswith('R'):
                    raise PDFError("cannot auto-compress streams whose /Filter property is an indirect object")
                elif f.startswith('['):
                    f = "[ /FlateDecode " + f[1:].strip()
                else:
                    f = "[ /FlateDecode %s ]" % f
                self.contents["Filter"] = f
                self.compress = zlib.compressobj(9)
            self.in_dict = True
            self.write_dict()
            self.write("stream\n")
            self.in_dict = False
            self.in_stream = True
            self.length = 0
        if not data:
            return
        if self.compress:
            data = self.compress.compress(data)
        PDFObj.write(self, data)
        self.length += len(data)

    def close(self):
        """ Flush and close stream data. """
        self.in_dict = True
        if not self.in_stream:
            self.write_dict()
            self.write("stream\n")
        if self.compress:
            data = self.compress.flush()
            PDFObj.write(self, data)
            self.length += len(data)
        if self.length:
            PDFObj.write(self, "\n")
        self.write("endstream\n")
        PDFObj.close(self)

class _pdf_stream_length(PDFObj):
    def __init__(self, parent_file, parent_stream):
        PDFObj.__init__(self, parent_file)
        self.s = parent_stream

    def close(self):
        self.write("%d\n" % self.s.length)
        PDFObj.close(self)

class PDFPage(object):
    """ PDF page object.
    This class acts as a container for all objects associated to pages:
    The page object itself, a dictionary of external objects (e.g. images)
    used by this page and the page content stream. Calling write() on this
    object will write directly to the content stream.
    """

    def __init__(self, parent_file):
        """ Constructor.
        - parent_file: the PDF file the page shall be added to
        """
        self.f = parent_file
        self.page_obj = PDFObj(self.f)
        self.xobjects = PDFDict(self.f)
        self.contents = PDFStream(self.f)
        self.write = self.contents.write
        self.f.pages.append(self)
        self.page_obj.write("<<\n/Type /Page\n/Parent %s\n/Contents %s\n/Resources <<\n/ProcSet [ /PDF /Text /ImageB /ImageC ]\n/XObject %s\n>> >>\n" % (self.f.pagetree.ref(), self.contents.ref(), self.xobjects.ref()))

    def use(self, obj):
        """ Generate a reference to an external object.
        - obj: the object to refer to
        Adds a reference to 'obj' in the XObjects dictionary of this page
        and returns a string containing the name of the object in the
        current page's context.
        """
        ref = obj.ref()
        name = "/R%d" % ref.obj
        self.xobjects[name] = ref
        return name

    def ref(self):
        """ Generate a reference to this page's page object. """
        return PDFRef(self.page_obj)

class PDFImage(PDFStream):
    """ PDF image object.
    This class represents an image, loaded from an external file or a file
    stream, that is part of a certain PDF file and can be put on one or
    more pages of the file.
    Only monochrome (1-bit), grayscale, RGB and CMYK images are supported.
    Other formats will be converted to one of the supported formats first.
    All images are rendered in their respective device colorspace
    (/DeviceGray, /DeviceRGB or /DeviceCMYK).
    """

    def __init__(self, parent_file, f, dpi=None, smooth=None, jpeg=None, quality=90):
        """ Constructor.
        - parent_file: the PDF file this image shall be part of
        - f: the image; can be either a file name, a file-like object, or a
             PIL image (Image.Image descendant)
        - dpi: the resolution of the image in dots per inch; can be either
               a single number or a (x,y) tuple; if omitted, the resolution
               is auto-detected; if even that fails, 72 dpi are assumed
        - smooth: set to True if the image shall be filtered when zoomed
                  in, or false if not; default value: True for grayscale
                  and color images, False for monochrome images
        - jpeg: set to True if the image shall be JPEG-encoded;
                set to False if the image shall be losslessly compressed
                (even if it was a JPEG file on disk);
                if omitted, JPEG files will be added to the PDF file
                verbatim and other files will be losslessly compressed
        - quality: JPEG quality level (1..99) if jpeg=True
        """
        PDFStream.__init__(self, parent_file, Type="/XObject", Subtype="/Image")
        if type(f) in (types.StringType, types.UnicodeType):
            f = open(f, "rb")
        elif _is_class(f.__class__, Image.Image):
            img = f
            f = None
        if f:
            img = Image.open(f)
        # convert to known color space
        if img.mode in ("P", "RGBA", "RGBa", "RGBX", "YCbCr") or (jpeg and (img.mode == "CMYK")):
            img = img.convert("RGB")
        elif img.mode in ("I", "F", "LA") or (jpeg and (img.mode == "1")):
            img = img.convert("L")
        # set up image parameters
        self["Width"], self["Height"] = img.size
        try:
            self["ColorSpace"], self["BitsPerComponent"] = {
                "1": ("/DeviceGray", 1),
                "L": ("/DeviceGray", 8),
                "RGB": ("/DeviceRGB", 8),
                "CMYK": ("/DeviceCMYK", 8),
            }[img.mode]
        except KeyError:
            raise PDFError("unsupported colorspace '%s'" % img.mode)
        if smooth is None:
            smooth = (self["BitsPerComponent"] >= 8)
        if smooth:
            self["Interpolate"] = "true"
        # determine size and resolution
        self.size = img.size
        if not dpi:
            self.dpi = img.info.get('dpi', (72, 72))
        elif type(dpi) in (types.TupleType, types.ListType):
            self.dpi = dpi
        else:
            self.dpi = (dpi, dpi)
        # write the image
        self.flush()
        if (jpeg is None) and f and _is_class(img.__class__, JpegImagePlugin.JpegImageFile):
            # JPEG file -> process directly
            del img
            self["Filter"] = "/DCTDecode"
            f.seek(0)
            marker = f.read(2)
            if marker != "\xff\xd8":
                raise PDFError("corrupt JPEG file")
            self.write(marker)
            while True:
                marker = f.read(2)
                if (len(marker) != 2) or (marker[0] != "\xff"):
                    raise PDFError("corrupt JPEG file")
                if not (ord(marker[1]) in range(0xE0, 0xF0) + [0xFE]):
                    break
                length = map(ord, f.read(2))
                if (len(length) != 2):
                    raise PDFError("corrupt JPEG file")
                f.read(length[0] * 255 + length[1] - 2)
            self.write(marker)
            while True:
                buf = f.read(4096)
                if not buf:
                    break  # EOF
                self.write(buf)
        elif jpeg:
            # encode file as JPEG
            self["Filter"] = "/DCTDecode"
            io = cStringIO.StringIO()
            img.save(io, "JPEG", quality=quality)
            del img
            self.write(io.getvalue())
            del io
        else:
            # no JPEG
            self.compress = True
            self.write(img.tostring())
            del img
        self.close()

    def put(self, page=None, pos=None, size=None, rotate=0):
        """ Put the image onto a page.
        - page: the page to put this image on; if omitted, a new page will
                be added to the PDF file this image belongs to
        - pos: a (x,y) tuple containing the coordinates of the upper-left
               corner of the image relative to the upper-left corner of the
               page, in millimeters;
               if omitted, the image will be centered on the page
        - size: the size of the image in millimeters;
                if it's a single number, the image will be scaled such that
                the longest side is as long as specified;
                it it's a (x,y) tuple, the image will be scaled as
                specified, even if the aspect ratio will be distorted;
                if either x or y is 0 (zero), the image will be scaled such
                that the other dimension fits to the specified length;
                if it's omitted, the image's resolution will be used to
                determine the original print size
        - rotate: amount of rotation in 90-degree steps, clockwise
        """
        # create page, if none specified
        if not page:
            page = PDFPage(self.f)
        # compute rotation
        isize = self.size
        idpi = self.dpi
        if abs(rotate) >= 90:
            if abs(rotate) % 90:
                raise PDFError("Image rotation is only supported for multiples of 90 degrees")
            rotate /= 90
        if rotate < 0:
            rotate += 0x7FFFFFFF
        rotate = rotate & 3
        transpose = rotate & 1
        if transpose:
            isize = (isize[1], isize[0])
            idpi = (idpi[1], idpi[0])
        # compute image size in points
        if not size:
            size = (72.0 * isize[0] / idpi[0], 72.0 * isize[1] / idpi[1])
        else:
            if not(type(size) in (types.TupleType, types.ListType)):
                # convert scalar image size to (x,0) or (0,y) tuple
                if isize[0] > isize[1]:
                    size = (size, 0)
                else:
                    size = (0, size)
            if not size[0]:
                size = mm2pt(size[1])
                size = (size * isize[0] / isize[1], size)
            elif not size[1]:
                size = mm2pt(size[0])
                size = (size, size * isize[1] / isize[0])
            else:
                size = tuple(map(mm2pt, size))
        # compute position
        if pos:
            pos = (mm2pt(pos[0]), mm2pt(self.f.pagesize[1] - pos[1]) - size[1])
        else:
            pos = (0.5 * (mm2pt(self.f.pagesize[0]) - size[0]), 0.5 * (mm2pt(self.f.pagesize[1]) - size[1]))
        # apply rotation
        if rotate == 1:
            pos = (pos[0], pos[1] + size[1])
            size = (size[0], -size[1])
        elif rotate == 2:
            pos = (pos[0] + size[0], pos[1] + size[1])
            size = (-size[0], -size[1])
        elif rotate == 3:
            pos = (pos[0] + size[0], pos[1])
            size = (-size[0], size[1])
        # write image
        if transpose:
            page.write("q 0 %s %s 0 %s %s cm %s Do Q\n" % (size[1], size[0], pos[0], pos[1], page.use(self)))
        else:
            page.write("q %s 0 0 %s %s %s cm %s Do Q\n" % (size[0], size[1], pos[0], pos[1], page.use(self)))

################################################################################

class ParamDict(object):
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
    def copy(self):
        return ParamDict(**self.__dict__)
    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, ", ".join(["%s=%r" % i for i in self.__dict__.iteritems()]))

def errexit(msg, code=1, hint=False):
    print >>sys.stderr, "error: %s" % msg
    if hint:
        print >>sys.stderr, "use '%s -h' to get help" % os.path.basename(sys.argv[0])
    sys.exit(code)

class CommandLineReader(object):
    def __init__(self, opts):
        self.argv = sys.argv
        self.argp = 1
        self.opts = {}
        lastopt = None
        for o in opts:
            if (o == ':') and lastopt:
                self.opts[lastopt] = True
            else:
                lastopt = o
                self.opts[lastopt] = False
    def __nonzero__(self):
        return self.argp < len(self.argv)
    def err(self, msg):
        errexit(msg, code=2)
    def next(self):
        if self.argp < len(self.argv):
            self.argp += 1
            return self.argv[self.argp - 1]
        else:
            self.err("unexpected end of command line")
    def getopt(self):
        opt = self.next()
        if (len(opt) < 2) or (opt[0] != '-'):
            return (None, opt)
        arg = opt[2:]
        opt = opt[1]
        if not opt in self.opts:
            self.err("invalid option '-%s'" % opt)
        if self.opts[opt]:
            if arg:
                return (opt, arg)
            else:
                return (opt, self.next())
        else:
            if arg:
                self.err("unexpected argument for option '-%s'" % opt)
            else:
                return (opt, None)
    def str2num(self, s):
        try:
            return int(s)
        except ValueError:
            try:
                return float(s)
            except ValueError:
                self.err("invalid number '%s'" % s)
    def parse_num(self, s, tuple_required=False):
        t = s.split(',')
        if len(t) == 1:
            t = s.split('x')
        if len(t) > 2:
            self.err("too many numeric arguments in '%s'" % s)
        if len(t) < 2:
            if tuple_required:
                self.err("coordinate required")
            else:
                return self.str2num(s)
        else:
            return tuple(map(self.str2num, t))

if __name__ == "__main__":
    # parse command line
    pages = []
    output = None
    explicit_pages = False
    verbose = False
    pagesize = (210, 297)
    meta = {}
    all = ParamDict(dpi=None, pos=None, size=None, rotate=0)
    once = all.copy()
    cmdline = CommandLineReader("hpm:vo:f:d:D:cCx:X:s:S:r:R:")
    while cmdline:
        opt, arg = cmdline.getopt()
        if not opt:
            if not explicit_pages:
                pages.append([])
            once.filename = arg
            pages[-1].append(once)
            once = all.copy()
        elif opt == "h":
            print __help__.replace("$prog", os.path.basename(sys.argv[0]))
            sys.exit(0)
        elif opt == "p":
            explicit_pages = True
            pages.append([])
        elif opt == "m":
            try:
                key, value = arg.split('=')
            except ValueError:
                errexit("malformed meta data argument", code=2)
            if not PDFInfoKeyAllowed(key):
                errexit("key '%s' not allowed as PDF metadata" % key, code=2)
            meta[key] = value
        elif opt == "v": verbose = True
        elif opt == "o": output = arg
        elif opt == "f": pagesize = cmdline.parse_num(arg, tuple_required=True)
        elif opt == "d": once.dpi = cmdline.parse_num(arg)
        elif opt == "D": once.dpi = all.dpi = cmdline.parse_num(arg)
        elif opt == "c": once.pos = None
        elif opt == "C": once.pos = all.pos = None
        elif opt == "x": once.pos = cmdline.parse_num(arg, tuple_required=True)
        elif opt == "X": once.pos = all.pos = cmdline.parse_num(arg, tuple_required=True)
        elif opt == "s": once.size = cmdline.parse_num(arg)
        elif opt == "S": once.size = all.size = cmdline.parse_num(arg)
        elif opt == "r": once.rotate = cmdline.str2num(arg)
        elif opt == "R": once.rotate = all.rotate = cmdline.str2num(arg)

    # create input file list
    filelist = []
    for page in pages:
        filelist.extend([img.filename for img in page])
    if not filelist:
        errexit("no input images", code=2, hint=True)

    # determine output file name
    if not output:
        if len(filelist) == 1:
            output = os.path.splitext(filelist[0])[0] + ".pdf"
        else:
            output = "pdfgen_output.pdf"
        print "Note: output file is '%s'" % output

    # create the file
    if verbose: print "creating output file:",
    try:
        pdf = PDFFile(output, pagesize)
    except (IOError, PDFError), e:
        errexit("cannot open output file: " + e.strerror)
    if verbose: print "OK"

    # apply metadata
    for key, value in meta.iteritems():
        pdf[key] = value

    # create pages and put images
    imgs = {}
    pagecount = 0
    for page_imgs in pages:
        pagecount += 1
        if verbose: print "creating page", pagecount
        page = PDFPage(pdf)
        for i in page_imgs:
            ikey = (i.filename, i.dpi)
            if ikey in imgs:
                img = imgs[ikey]
            else:
                if verbose: print "importing image '%s'" % i.filename
                try:
                    img = PDFImage(pdf, i.filename, dpi=i.dpi)
                except (IOError, PDFError), e:
                    errexit("cannot open image '%s': %s" % (i.filename, e.strerror))
                imgs[ikey] = img
            if verbose: print "putting image '%s' on page %d" % (i.filename, pagecount)
            try:
                img.put(page, pos=i.pos, size=i.size, rotate=i.rotate)
            except (IOError, PDFError), e:
                errexit("cannot put image '%s': %s" % (i.filename, e.strerror))
    del imgs

    # done.
    if verbose: print "closing output file:",
    try:
        pdf.close()
    except (IOError, PDFError), e:
        errexit("cannot close output file: " + e.strerror)
    if verbose: print "OK"

