#pdfgen.py
"""
PDFgen is a library to generate PDF files containing text and graphics.  It is the
foundation for a complete reporting solution in Python.  It is also the
foundation for piddlePDF, the PDF back end for PIDDLE.

Documentation is a little slim right now; run then look at testpdfgen.py
to get a clue.

---------- Licence Terms (same as the Python license) -----------------
(C) Copyright Robinson Analytics 1998-1999.

Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted, provided
that the above copyright notice appear in all copies and that both that
copyright notice and this permission notice appear in supporting
documentation, and that the name of Robinson Analytics not be used
in advertising or publicity pertaining to distribution of the software
without specific, written prior permission.

ROBINSON ANALYTICS LTD. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS,
IN NO EVENT SHALL ROBINSON ANALYTICS BE LIABLE FOR ANY SPECIAL, INDIRECT
OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

Progress Reports:

Modified 7/25/2006 read rooberts. Added supported for embedding fonts.

0.82, 1999-10-27, AR:
        Fixed some bugs on printing to Postscript.  Added 'Text Object'
        analogous to Path Object to control entry and exit from text mode.
        Much simpler clipping API.  All verified to export Postscript and
        redistill.
        One limitation still - clipping to text paths is fine in Acrobat
        but not in Postscript (any level)

0.81,1999-10-13, AR:
        Adding RoundRect; changed all format strings to use %0.2f instead of %s,
        so we don't get exponentials in the output.
0.8,1999-10-07, AR:  all changed!

2000-02-07: changed all %0.2f's to %0.4f in order to allow precise ploting of graphs that
        range between 0 and 1 -cwl

"""
# 1
##  0.81    1999-10-13:
##
##
##

import os
from io import StringIO
from math import sin, cos, tan, pi

from afdko.pdflib import pdfdoc, pdfgeom, pdfmetrics, pdfutils

# Robert Kern
# Constants for closing paths.
# May be useful if one changes 'arc' and 'rect' to take a
# default argument that tells how to close the path.
# That way we can draw filled shapes.

FILL_EVEN_ODD = 0
FILL_NON_ZERO = 1
    #this is used by path-closing routines.
    #map stroke, fill, fillmode -> operator
    # fillmode: 1 = non-Zero (obviously), 0 = evenOdd
PATH_OPS = {(0, 0, FILL_EVEN_ODD) : 'n',  #no op
            (0, 0, FILL_NON_ZERO) : 'n',  #no op
            (1, 0, FILL_EVEN_ODD) : 'S',  #stroke only
            (1, 0, FILL_NON_ZERO) : 'S',  #stroke only
            (0, 1, FILL_EVEN_ODD) : 'f*',  #Fill only
            (0, 1, FILL_NON_ZERO) : 'f',  #Fill only
            (1, 1, FILL_EVEN_ODD) : 'B*',  #Stroke and Fill
            (1, 1, FILL_NON_ZERO) : 'B',  #Stroke and Fill
            }

close = 'h'
newpath = 'n'
stroke = 'S'
closeStroke = 's'
nzFill = 'f'
eoFill = 'f*'
fillStroke = 'B'
closeFillStroke = 'b'
eoFillStroke = 'B*'
closeEoFillStroke = 'b*'


class PDFError(Exception):
    pass


class Canvas:
    """This is a low-level interface to the PDF file format.  The plan is to
    expose the whole pdfgen API through this.  Its drawing functions should have a
    one-to-one correspondence with PDF functionality.  Unlike PIDDLE, it thinks
    in terms of RGB values, Postscript font names, paths, and a 'current graphics
    state'.  Just started development at 5/9/99, not in use yet.

    """
    def __init__(self,filename,pagesize=(595.27,841.89), bottomup = 1):
        """Most of the attributes are private - we will use set/get methods
        as the preferred interface.  Default page size is A4."""
        self._filename = filename
        self._doc = pdfdoc.PDFDocument()
        self._pagesize = pagesize
        self._currentPageHasImages = 1
        self._pageTransitionString = ''

        self._pageCompression = 1  # on by default - turn off when debugging!
        self._pageNumber = 1   # keep a count
        # where the current page's marking operators accumulate
        self._code = []

        # PostScript has the origin at bottom left. It is easy to achieve a
        # top-down coord system by translating to the top of the page and
        # setting y scale to -1, but then text is inverted. So self.bottomup
        # is used to also set the text matrix accordingly. You can now choose
        # your drawing coordinates.
        self.bottomup = bottomup
        if self.bottomup:
            # set initial font
            # self._preamble = 'BT /F9 12 Tf 14.4 TL ET'
            self._preamble = '1 0 0 1 0 0 cm BT /F9 12 Tf 14.4 TL ET'
        else:
            # switch coordinates, flip text and set font
            self._preamble = ('1 0 0 -1 0 %0.4f cm BT /F9 12 Tf 14.4 TL ET' %
                              self._pagesize[1])

        # initial graphics state
        self._x = 0
        self._y = 0
        self._fontname = 'Times-Roman'
        self._fontsize = 12
        self._textMode = 0  # track if between BT/ET
        self._leading = 14.4
        self._currentMatrix = (1., 0., 0., 1., 0., 0.)
        self._fillMode = 0  # even-odd

        # text state
        self._charSpace = 0
        self._wordSpace = 0
        self._horizScale = 100
        self._textRenderMode = 0
        self._rise = 0
        self._textLineMatrix = (1., 0., 0., 1., 0., 0.)
        self._textMatrix = (1., 0., 0., 1., 0., 0.)

        # line drawing
        self._lineCap = 0
        self._lineJoin = 0
        self._lineDash = None  # not done
        self._lineWidth = 0
        self._mitreLimit = 0

        self._fillColorRGB = (0,0,0)
        self._strokeColorRGB = (0,0,0)

    def _escape(self, s):
        """PDF escapes are like Python ones, but brackets need slashes before them too.
        Use Python's repr function and chop off the quotes first"""
        # s = repr(s)[1:-1]
        s = s.replace('(', r'\(')
        s = s.replace(')', r'\)')
        return s

    # info functions - non-standard
    def setAuthor(self, author):
        self._doc.setAuthor(author)

    def setTitle(self, title):
        self._doc.setTitle(title)

    def setSubject(self, subject):
        self._doc.setSubject(subject)

    def pageHasData(self):
        "Info function - app can call it after showPage to see if it needs a save"
        return len(self._code) == 0

    def showPage(self):
        """This is where the fun happens"""
        page = pdfdoc.PDFPage()
        page.pagewidth = self._pagesize[0]
        page.pageheight = self._pagesize[1]
        page.hasImages = self._currentPageHasImages
        page.pageTransitionString = self._pageTransitionString
        page.setCompression(self._pageCompression)
        # print stream
        page.setStream([self._preamble] + self._code)
        self._doc.addPage(page)

        # now get ready for the next one
        self._pageNumber = self._pageNumber + 1
        self._code = []    # ready for more...
        self._currentPageHasImages = 0

    def getPageNumber(self):
        return self._pageNumber

    def save(self, filename=None, fileobj=None):

        """Saves the pdf document to fileobj or to file with name filename.
        If holding data, do a showPage() to save them having to."""

        if len(self._code):
            self.showPage()  # what's the effect of multiple 'showPage's
        if fileobj:
            self._doc.SaveToFileObject(fileobj)
        elif filename:
            self._doc.SaveToFile(filename)
        else:
            self._doc.SaveToFile(self._filename)

    def setPageSize(self, size):
        """accepts a 2-tuple in points for paper size for this
        and subsequent pages"""
        self._pagesize = size

    def addLiteral(self, s, escaped=1):
        if escaped == 0:
            s = self._escape(s)
        self._code.append(s)


        ######################################################################
        #
        #      coordinate transformations
        #
        ######################################################################


    def transform(self, a,b,c,d,e,f):
        """How can Python track this?"""
        a0,b0,c0,d0,e0,f0 = self._currentMatrix
        self._currentMatrix = (a0*a+c0*b,    b0*a+d0*b,
                               a0*c+c0*d,    b0*c+d0*d,
                               a0*e+c0*f+e0, b0*e+d0*f+f0)
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f cm' % (a,b,c,d,e,f))

    def translate(self, dx, dy):
        self.transform(1,0,0,1,dx,dy)

    def scale(self, x, y):
        self.transform(x,0,0,y,0,0)

    def rotate(self, theta):
        """Canvas.rotate(theta)

        theta is in degrees."""
        c = cos(theta * pi / 180)
        s = sin(theta * pi / 180)
        self.transform(c, s, -s, c, 0, 0)

    def skew(self, alpha, beta):
        tanAlpha = tan(alpha * pi / 180)
        tanBeta  = tan(beta  * pi / 180)
        self.transform(1, tanAlpha, tanBeta, 1, 0, 0)

        ######################################################################
        #
        #      graphics state management
        #
        ######################################################################



    def saveState(self):
        """These need expanding to save/restore Python's state tracking too"""
        self._code.append('q')

    def restoreState(self):
        """These need expanding to save/restore Python's state tracking too"""
        self._code.append('Q')

        ###############################################################
        #
        #   Drawing methods.  These draw things directly without
        #   fiddling around with Path objects.  We can add any geometry
        #   methods we wish as long as their meaning is precise and
        #   they are of general use.
        #
        #   In general there are two patterns.  Closed shapes
        #   have the pattern shape(self, args, stroke=1, fill=0);
        #   by default they draw an outline only. Line segments come
        #   in three flavours: line, bezier, arc (which is a segment
        #   of an elliptical arc, approximated by up to four bezier
        #   curves, one for each quadrant.
        #
        #   In the case of lines, we provide a 'plural' to unroll
        #   the inner loop; it is useful for drawing big grids
        ################################################################


        #--------first the line drawing methods-----------------------
    def line(self, x1,y1, x2,y2):
        "As it says"
        self._code.append('n %0.4f %0.4f m %0.4f %0.4f l S' % (x1, y1, x2, y2))

    def lines(self, linelist):
        """As line(), but slightly more efficient for lots of them -
        one stroke operation and one less function call"""
        self._code.append('n')
        for (x1,y1,x2,y2) in linelist:
            self._code.append('%0.4f %0.4f m %0.4f %0.4f l' % (x1, y1, x2, y2))
        self._code.append('S')

    def grid(self, xlist, ylist):
        """Lays out a grid in current line style.  Suuply list of
        x an y positions."""
        assert len(xlist) > 1, "x coordinate list must have 2+ items"
        assert len(ylist) > 1, "y coordinate list must have 2+ items"
        lines = []
        y0, y1 = ylist[0], ylist[-1]
        x0, x1 = xlist[0], xlist[-1]
        for x in xlist:
            lines.append(x,y0,x,y1)
        for y in ylist:
            lines.append(x0,y,x1,y)
        self.lines(lines)

    def bezier(self, x1, y1, x2, y2, x3, y3, x4, y4):
        "Bezier curve with the four given control points"
        self._code.append('n %0.4f %0.4f m %0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c S' %
                          (x1, y1, x2, y2, x3, y3, x4, y4)
                          )
    def arc(self, x1,y1, x2,y2, startAng=0, extent=90):
        """Contributed to piddlePDF by Robert Kern, 28/7/99.
        Trimmed down by AR to remove color stuff for pdfgen.canvas and
        revert to positive coordinates.

        Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2,
        starting at startAng degrees and covering extent degrees.   Angles
        start with 0 to the right (+x) and increase counter-clockwise.
        These should have x1<x2 and y1<y2.

        The algorithm is an elliptical generalization of the formulae in
        Jim Fitzsimmon's TeX tutorial <URL: http://www.tinaja.com/bezarc1.pdf>."""

        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent)
        # move to first point
        self._code.append('n %0.4f %0.4f m' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])
        # stroke
        self._code.append('S')

        #--------now the shape drawing methods-----------------------
    def rect(self, x, y, width, height, stroke=1, fill=0):
        "draws a rectangle"
        self._code.append('n %0.4f %0.4f %0.4f %0.4f re ' % (x, y, width, height)
                          + PATH_OPS[stroke, fill, self._fillMode])


    def ellipse(self, x1, y1, x2, y2, stroke=1, fill=0):
        """Uses bezierArc, which conveniently handles 360 degrees -
        nice touch Robert"""
        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, 0, 360)
        #move to first point
        self._code.append('n %0.4f %0.4f m' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])
        #finish
        self._code.append(PATH_OPS[stroke, fill, self._fillMode])


    def wedge(self, x1,y1, x2,y2, startAng, extent, stroke=1, fill=0):
        """Like arc, but connects to the centre of the ellipse.
        Most useful for pie charts and PacMan!"""

        x_cen  = (x1+x2)/2.
        y_cen  = (y1+y2)/2.
        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent)

        self._code.append('n %0.4f %0.4f m' % (x_cen, y_cen))
        # Move the pen to the center of the rectangle
        self._code.append('%0.4f %0.4f l' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])
        # finish the wedge
        self._code.append('%0.4f %0.4f l ' % (x_cen, y_cen))
        # final operator
        self._code.append(PATH_OPS[stroke, fill, self._fillMode])

    def circle(self, x_cen, y_cen, r, stroke=1, fill=0):
        """special case of ellipse"""
        x1 = x_cen - r
        x2 = x_cen + r
        y1 = y_cen - r
        y2 = y_cen + r
        self.ellipse(x1, y1, x2, y2, stroke, fill)

    def roundRect(self, x, y, width, height, radius, stroke=1, fill=0):
        """Draws a rectangle with rounded corners.  The corners are
        approximately quadrants of a circle, with the given radius."""
        #use a precomputed set of factors for the bezier approximation
        #to a circle. There are six relevant points on the x axis and y axis.
        #sketch them and it should all make sense!
        t = 0.4472 * radius

        x0 = x
        x1 = x0 + t
        x2 = x0 + radius
        x3 = x0 + width - radius
        x4 = x0 + width - t
        x5 = x0 + width

        y0 = y
        y1 = y0 + t
        y2 = y0 + radius
        y3 = y0 + height - radius
        y4 = y0 + height - t
        y5 = y0 + height

        self._code.append('n %0.4f %0.4f m' % (x2, y0))
        self._code.append('%0.4f %0.4f l' % (x3, y0))  # bottom row
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c'
                         % (x4, y0, x5, y1, x5, y2)) # bottom right

        self._code.append('%0.4f %0.4f l' % (x5, y3))  # right edge
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c'
                         % (x5, y4, x4, y5, x3, y5)) # top right

        self._code.append('%0.4f %0.4f l' % (x2, y5))  # top row
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c'
                         % (x1, y5, x0, y4, x0, y3)) # top left

        self._code.append('%0.4f %0.4f l' % (x0, y2))  # left edge
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c'
                         % (x0, y1, x1, y0, x2, y0)) # bottom left

        self._code.append('h')  #close off, although it should be where it started anyway


        self._code.append(PATH_OPS[stroke, fill, self._fillMode])
        ##################################################
        #
        #  Text methods
        #
        # As with graphics, a separate object ensures that
        # everything is bracketed between  text operators.
        # The methods below are a high-level convenience.
        # use PDFTextObject for multi-line text.
        ##################################################

    def drawString(self, x, y, text):
        """Draws a string in the current text styles."""
        #we could inline this for speed if needed
        t = self.beginText(x, y)
        t.textLine(text)
        self.drawText(t)

    def drawRightString(self, x, y, text):
        """Draws a string right-aligned with the y coordinate"""
        width = self.stringWidth(text, self._fontname, self._fontsize)
        t = self.beginText(x - width, y)
        t.textLine(text)
        self.drawText(t)

    def drawCentredString(self, x, y, text):
        """Draws a string right-aligned with the y coordinate.  I
        am British so the spelling is correct, OK?"""
        width = self.stringWidth(text, self._fontname, self._fontsize)
        t = self.beginText(x - 0.5 * width, y)
        t.textLine(text)
        self.drawText(t)

    def getAvailableFonts(self):
        """Returns the list of PostScript font names available.
        Standard set now, but may grow in future with font embedding."""
        fontEntries = sorted(self._doc.getAvailableFonts())
        return fontEntries  # list of (psName, Encoding)

    def addFont(self, psfontname, encoding=None, clientCtx=None,
                getFontDescriptor=None, getEncodingInfo=None):
        # this function will not add the font or
        # font descriptor if they already exist
        self._doc.addFont(psfontname, encoding, clientCtx, getFontDescriptor,
                          getEncodingInfo)

    def setFont(self, psfontname, size, leading=None, encoding=None):
        """Sets the font.  If leading not specified, defaults to 1.2 x
        font size. Raises a readable exception if an illegal font
        is supplied.  Font names are case-sensitive! Keeps track
        of font anme and size for metrics."""
        self._fontname = psfontname
        self._fontsize = size
        pdffontname = self._doc.getInternalFontName(psfontname, encoding)
        if leading is None:
            leading = size * 1.2
        self._leading = leading
        self._code.append('BT %s %0.1f Tf %0.1f TL ET' % (pdffontname, size, leading))

    def stringWidth(self, text, fontname, fontsize):
        "gets width of a string in the given font and size"
        return pdfmetrics.stringwidth(text, fontname) * 0.001 * fontsize

    # basic graphics modes
    def setLineWidth(self, width):
        self._lineWidth = width
        self._code.append('%0.4f w' % width)

    def setLineCap(self, mode):
        """0=butt,1=round,2=square"""
        assert mode in (0,1,2), "Line caps allowed: 0=butt,1=round,2=square"
        self._lineCap = mode
        self._code.append('%d J' % mode)

    def setLineJoin(self, mode):
        """0=mitre, 1=round, 2=bevel"""
        assert mode in (0,1,2), "Line Joins allowed: 0=mitre, 1=round, 2=bevel"
        self._lineJoin = mode
        self._code.append('%d j' % mode)

    def setMiterLimit(self, limit):
        self._miterLimit = limit
        self._code.append('%0.4f M' % limit)

    def setDash(self, array=None, phase=0):
        """Two notations.  pass two numbers, or an array and phase"""
        if isinstance(array, int) or isinstance(array, float):
            self._code.append('[%s %s] 0 d' % (array, phase))
        elif isinstance(array, list) or isinstance(array, tuple):
            assert phase <= len(array), "setDash phase must be l.t.e. length of array"
            textarray = ''.join(map(str, array))
            self._code.append('[%s] %s d' % (textarray, phase))

    def setFillColorRGB(self, r, g, b):
        self._fillColorRGB = (r, g, b)
        self._code.append('%0.4f %0.4f %0.4f rg' % (r,g,b))

    def setStrokeColorRGB(self, r, g, b):
        self._strokeColorRGB = (r, g, b)
        self._code.append('%0.4f %0.4f %0.4f RG' % (r,g,b))

    # path stuff - the separate path object builds it
    def beginPath(self):
        """Returns a fresh path object"""
        return PDFPathObject()

    def drawPath(self, aPath, stroke=1, fill=0):
        "Draw in the mode indicated"
        op = PATH_OPS[stroke, fill, self._fillMode]
        self._code.append(aPath.getCode() + ' ' + op)

    def clipPath(self, aPath, stroke=1, fill=0):
        "clip as well as drawing"
        op = PATH_OPS[stroke, fill, self._fillMode]
        self._code.append(aPath.getCode() + ' W ' + op)

    def beginText(self, x=0, y=0):
        """Returns a fresh text object"""
        return PDFTextObject(self, x, y)

    def drawText(self, aTextObject):
        """Draws a text object"""
        self._code.append(aTextObject.getCode())

        ######################################################
        #
        #   Image routines
        #
        ######################################################
    def drawInlineImage(self, image, x,y, width=None,height=None):
        """Draw a PIL Image into the specified rectangle.  If width and
        height are omitted, they are calculated from the image size.
        Also allow file names as well as images.  This allows a
        caching mechanism"""
        # print "drawInlineImage: x=%s, y=%s, width = %s, height=%s " % (x,y, width, height)
        import zlib

        self._currentPageHasImages = 1

        if isinstance(image, str):
            if os.path.splitext(image)[1] in ['.jpg', '.JPG']:
            #directly process JPEG files
            #open file, needs some error handling!!
                imageFile = open(image, 'rb')
                info = self.readJPEGInfo(imageFile)
                imgwidth, imgheight = info[0], info[1]
                if info[2] == 1:
                    colorSpace = 'DeviceGray'
                elif info[2] == 3:
                    colorSpace = 'DeviceRGB'
                else: #maybe should generate an error, is this right for CMYK?
                    colorSpace = 'DeviceCMYK'
                imageFile.seek(0)  # reset file pointer
                imagedata = []
                imagedata.append('BI')   # begin image
                # this describes what is in the image itself
                imagedata.append('/Width %0.4f /Height %0.4f' %(info[0], info[1]))
                imagedata.append('/BitsPerComponent 8')
                imagedata.append('/ColorSpace /%s' % colorSpace)
                imagedata.append('/Filter [ /ASCII85Decode /DCTDecode]')
                imagedata.append('ID')
                #write in blocks of (??) 60 characters per line to a list
                compressed = imageFile.read()
                encoded = pdfutils._AsciiBase85Encode(compressed)
                outstream = StringIO(encoded)
                dataline = outstream.read(60)
                while dataline != "":
                    imagedata.append(dataline)
                    dataline = outstream.read(60)
                imagedata.append('EI')
            else:
                if not pdfutils.cachedImageExists(image):
                    pdfutils.cacheImageFile(image)
                #now we have one cached, slurp it in
                cachedname = os.path.splitext(image)[0] + '.a85'
                imagedata = open(cachedname,'rb').readlines()
                #trim off newlines...
                imagedata = list(map(str.strip, imagedata))

                #parse line two for width, height
                words = imagedata[1].split()
                imgwidth = int(words[1])
                imgheight = int(words[3])
        else:
            #PIL Image
            #work out all dimensions
            myimage = image.convert('RGB')
            imgwidth, imgheight = myimage.size
            imagedata = []
            imagedata.append('BI')   # begin image

            # this describes what is in the image itself
            imagedata.append('/W %0.4f /H %0.4f /BPC 8 /CS /RGB /F [/A85 /Fl]' % (imgwidth, imgheight))
            imagedata.append('ID')

            #use a flate filter and Ascii Base 85 to compress
            raw = myimage.tostring()
            assert len(raw) == (imgwidth * imgheight), "Wrong amount of data for image"
            compressed = zlib.compress(raw)   #this bit is very fast...
            encoded = pdfutils._AsciiBase85Encode(compressed) #...sadly this isn't

            #write in blocks of (??) 60 characters per line to a list
            outstream = StringIO(encoded)
            dataline = outstream.read(60)
            while dataline != "":
                imagedata.append(dataline)
                dataline = outstream.read(60)
            imagedata.append('EI')

        #now build the PDF for the image.
        if not width:
            width = imgwidth
        if not height:
            height = imgheight

        # this says where and how big to draw it
        #self._code.append('ET')
        #self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, height, x, y+height))
        if self.bottomup:
            self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, height, x, y))
        else:
            # multiply  height by (-1) to overcome flip in coordinate system -cwl
            self._code.append('q %0.4f 0 0 %0.4f %0.4f %0.4f cm' % (width, -height, x, y+height))
        self._code.extend(imagedata)
        self._code.append('Q')
        #self._code.append('BT')

#########################################################################
#
#  JPEG processing code - contributed by Eric Johnson
#
#########################################################################

    # Read data from the JPEG file. We should probably be using PIL to
    # get this information for us -- but this way is more fun!
    # Returns (width, height, color components) as a triple
    # This is based on Thomas Merz's code from GhostScript (viewjpeg.ps)
    def readJPEGInfo(self, image):
        "Read width, height and number of components from JPEG file"
        import struct

        # Acceptable JPEG Markers:
        #  SROF0=baseline, SOF1=extended sequential or SOF2=progressive
        validMarkers = [0xC0, 0xC1, 0xC2]

        # JPEG markers without additional parameters
        noParamMarkers = [0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
                          0xD8, 0x01]

        # Unsupported JPEG Markers
        unsupportedMarkers = [0xC3, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB,
                              0xCD, 0xCE, 0xCF]

        # read JPEG marker segments until we find SOFn marker or EOF
        done = 0
        while not done:
            x = struct.unpack('B', image.read(1))
            if x[0] == 0xFF:  # found marker
                x = struct.unpack('B', image.read(1))
            # print "Marker: ", '%0.2x' % x[0]
            # check marker type is acceptable and process it
            if x[0] in validMarkers:
                image.seek(2, 1)  # skip segment length
                x = struct.unpack('B', image.read(1))  # data precision
                if x[0] != 8:
                    raise PDFError('JPEG must have 8 bits per component')
                y = struct.unpack('BB', image.read(2))
                height = (y[0] << 8) + y[1]
                y = struct.unpack('BB', image.read(2))
                width = (y[0] << 8) + y[1]
                y = struct.unpack('B', image.read(1))
                color = y[0]
                return width, height, color
            elif x[0] in unsupportedMarkers:
                raise PDFError('Unsupported JPEG marker: %0.2x' % x[0])
            elif x[0] not in noParamMarkers:
                # skip segments with parameters
                # read length and skip the data
                x = struct.unpack('BB', image.read(2))
                image.seek((x[0] << 8) + x[1] - 2, 1)

    def setPageCompression(self, onoff=1):
        """Possible values 1 or 0 (1 for 'on' is the default).
        If on, the page data will be compressed, leading to much
        smaller files, but takes a little longer to create the files.
        This applies to all subsequent pages, or until setPageCompression()
        is next called."""
        self._pageCompression = onoff

    def setPageTransition(self, effectname=None, duration=1,
                          direction=0, dimension='H', motion='I'):
        """PDF allows page transition effects for use when giving
        presentations.  There are six possible effects.  You can
        just guive the effect name, or supply more advanced options
        to refine the way it works.  There are three types of extra
        argument permitted, and here are the allowed values:
            direction_arg = [0,90,180,270]
            dimension_arg = ['H', 'V']
            motion_arg = ['I','O'] (start at inside or outside)

        This table says which ones take which arguments:

        PageTransitionEffects = {
            'Split': [direction_arg, motion_arg],
            'Blinds': [dimension_arg],
            'Box': [motion_arg],
            'Wipe' : [direction_arg],
            'Dissolve' : [],
            'Glitter':[direction_arg]
            }
        Have fun!
"""
        if not effectname:
            self._pageTransitionString = ''
            return

        #first check each optional argument has an allowed value
        if direction in [0,90,180,270]:
            direction_arg = '/Di /%d' % direction
        else:
            raise PDFError('directions allowed are 0, 90, 180, 270')

        if dimension in ['H', 'V']:
            dimension_arg = '/Dm /%s' % dimension
        else:
            raise PDFError('dimension values allowed are H and V')

        if motion in ['I','O']:
            motion_arg = '/M /%s' % motion
        else:
            raise PDFError('motion values allowed are I and O')


        # this says which effects require which argument types from above
        PageTransitionEffects = {
            'Split': [direction_arg, motion_arg],
            'Blinds': [dimension_arg],
            'Box': [motion_arg],
            'Wipe': [direction_arg],
            'Dissolve': [],
            'Glitter': [direction_arg]
            }

        try:
            args = PageTransitionEffects[effectname]
        except KeyError:
            raise PDFError('Unknown Effect Name "%s"' % effectname)

        self._pageTransitionString = (('/Trans <</D %d /S /%s ' % (duration, effectname)) +
            ' '.join(args) + ' >>')


class PDFPathObject:
    """Represents a graphic path.  There are certain 'modes' to PDF
    drawing, and making a separate object to expose Path operations
    ensures they are completed with no run-time overhead.  Ask
    the Canvas for a PDFPath with getNewPathObject(); moveto/lineto/
    curveto wherever you want; add whole shapes; and then add it back
    into the canvas with one of the relevant operators.

    Path objects are probably not long, so we pack onto one line"""
    def __init__(self):
        self._code = []
        self._code.append('n')   #newpath

    def getCode(self):
        "pack onto one line; used internally"
        return ' '.join(self._code)
    def moveTo(self, x, y):
        self._code.append('%0.4f %0.4f m' % (x,y))
    def lineTo(self, x, y):
        self._code.append('%0.4f %0.4f l' % (x,y))
    def curveTo(self, x1, y1, x2, y2, x3, y3):
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % (x1, y1, x2, y2, x3, y3))

    def arc(self, x1,y1, x2,y2, startAng=0, extent=90):
        """Contributed to piddlePDF by Robert Kern, 28/7/99.
        Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2,
        starting at startAng degrees and covering extent degrees.   Angles
        start with 0 to the right (+x) and increase counter-clockwise.
        These should have x1<x2 and y1<y2.

        The algorithm is an elliptical generalization of the formulae in
        Jim Fitzsimmon's TeX tutorial <URL: http://www.tinaja.com/bezarc1.pdf>."""

        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent)
        #move to first point
        self._code.append('%0.4f %0.4f m' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])

    def arcTo(self, x1,y1, x2,y2, startAng=0, extent=90):
        """Like arc, but draws a line from the current point to
        the start if the start is not the current point."""
        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent)
        self._code.append('%0.4f %0.4f l' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])


    def rect(self, x, y, width, height):
        """Adds a rectangle to the path"""
        self._code.append('%0.4f %0.4f %0.4f %0.4f re' % (x, y, width, height))

    def ellipse(self, x, y, width, height):
        """adds an ellipse to the path"""
        pointList = pdfgeom.bezierArc(x, y, x + width,y + height, 0, 360)
        self._code.append('%0.4f %0.4f m' % pointList[0][:2])
        for curve in pointList:
            self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f c' % curve[2:])

    def circle(self, x_cen, y_cen, r):
        """adds a circle to the path"""
        self.ellipse(x_cen - r, y_cen - r, x_cen + r, y_cen + r)

    def close(self):
        "draws a line back to where it started"
        self._code.append('h')


class PDFTextObject:
    """PDF logically separates text and graphics drawing; you can
    change the coordinate systems for text and graphics independently.
    If you do drawings while in text mode, they appear in the right places
    on the page in Acrobat Reader, bur when you export Postscript to
    a printer the graphics appear relative to the text coordinate
    system.  I regard this as a bug in how Acrobat exports to PostScript,
    but this is the workaround.  It forces the user to separate text
    and graphics.  To output text, ask te canvas for a text object
    with beginText(x, y).  Do not construct one directly. It keeps
    track of x and y coordinates relative to its origin."""
    def __init__(self, canvas, x=0,y=0):
        self._code = []
        self._code.append('BT')
        self._canvas = canvas  #canvas sets this so it has access to size info
        self._fontname = self._canvas._fontname
        self._fontsize = self._canvas._fontsize
        self._leading = self._canvas._leading

        self.setTextOrigin(x, y)

    def getCode(self):
        "pack onto one line; used internally"
        self._code.append('ET')
        return ' '.join(self._code)

    def setTextOrigin(self, x, y):
        if self._canvas.bottomup:
            self._code.append('1 0 0 1 %0.4f %0.4f Tm' % (x, y)) #bottom up
        else:
            self._code.append('1 0 0 -1 %0.4f %0.4f Tm' % (x, y))  #top down
        self._x = x
        self._y = y
        self._x0 = x #the margin

    def setTextTransform(self, a, b, c, d, e, f):
        "Like setTextOrigin, but does rotation, scaling etc."
        # flip "y" coordinate for top down coordinate system -cwl
        # (1  0)   (a  b)      ( a   b)
        # (0 -1)   (c  d)   =  (-c  -d)
        self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f Tm' % (a, b, -c, -d, e, f))  #top down

        #self._code.append('%0.4f %0.4f %0.4f %0.4f %0.4f %0.4f Tm' % (a, b, c, d, e, f)) #bottom up
        #we only measure coords relative to present text matrix
        self._x = e
        self._y = f

    def moveCursor(self, dx, dy):
        """Moves to a point dx, dy away from the start of the
        current line - NOT from the current point! So if
        you call it in mid-sentence, watch out."""
        self._code.append('%s %s Td' % (dx, -dy))

    def getCursor(self):
        """Returns current text position relative to the last origin."""
        return (self._x, self._y)

    def getX(self):
        """Returns current x position relative to the last origin."""
        return self._x

    def getY(self):
        """Returns current y position relative to the last origin."""
        return self._y

    def setFont(self, psfontname, size, leading=None):
        """Sets the font.  If leading not specified, defaults to 1.2 x
        font size. Raises a readable exception if an illegal font
        is supplied.  Font names are case-sensitive! Keeps track
        of font anme and size for metrics."""
        self._fontname = psfontname
        self._fontsize = size
        pdffontname = self._canvas._doc.getInternalFontName(psfontname)
        if leading is None:
            leading = size * 1.2
        self._leading = leading
        self._code.append('%s %0.1f Tf %0.1f TL' % (pdffontname, size, leading))

    def setCharSpace(self, charSpace):
        """Adjusts inter-character spacing"""
        self._charSpace = charSpace
        self._code.append('%0.4f Tc' % charSpace)

    def setWordSpace(self, wordSpace):
        """Adjust inter-word spacing.  This can be used
        to flush-justify text - you get the width of the
        words, and add some space between them."""
        self._wordSpace = wordSpace
        self._code.append('%0.4f Tw' % wordSpace)

    def setHorizScale(self, horizScale):
        "Stretches text out horizontally"
        self._horizScale = 100 + horizScale
        self._code.append('%0.4f Tz' % horizScale)

    def setLeading(self, leading):
        "How far to move down at the end of a line."
        self._leading = leading
        self._code.append('%0.4f TL' % leading)

    def setTextRenderMode(self, mode):
        """Set the text rendering mode.

        0 = Fill text
        1 = Stroke text
        2 = Fill then stroke
        3 = Invisible
        4 = Fill text and add to clipping path
        5 = Stroke text and add to clipping path
        6 = Fill then stroke and add to clipping path
        7 = Add to clipping path"""

        assert mode in (0,1,2,3,4,5,6,7), "mode must be in (0,1,2,3,4,5,6,7)"
        self._textRenderMode = mode
        self._code.append('%d Tr' % mode)

    def setRise(self, rise):
        "Move text baseline up or down to allow superscrip/subscripts"
        self._rise = rise
        self._y = self._y - rise    # + ?  _textLineMatrix?
        self._code.append('%0.4f Ts' % rise)

    def setStrokeColorRGB(self, r, g, b):
        self._strokeColorRGB = (r, g, b)
        self._code.append('%0.4f %0.4f %0.4f RG' % (r,g,b))

    def setFillColorRGB(self, r, g, b):
        self._fillColorRGB = (r, g, b)
        self._code.append('%0.4f %0.4f %0.4f rg' % (r,g,b))


    def textOut(self, text):
        "prints string at current point, text cursor moves across"
        text = self._canvas._escape(text)
        self._x = self._x + self._canvas.stringWidth(
                    text, self._fontname, self._fontsize)
        self._code.append('(%s) Tj' % text)

    def textLine(self, text=''):
        """prints string at current point, text cursor moves down.
        Can work with no argument to simply move the cursor down."""
        text = self._canvas._escape(text)
        self._x = self._x0
        if self._canvas.bottomup:
            self._y = self._y - self._leading
        else:
            self._y = self._y + self._leading
        self._code.append('(%s) Tj T*' % text)

    def textLines(self, stuff, trim=1):
        """prints multi-line or newlined strings, moving down.  One
        comon use is to quote a multi-line block in your Python code;
        since this may be indented, by default it trims whitespace
        off each line and from the beginning; set trim=0 to preserve
        whitespace."""
        if isinstance(stuff, str):
            lines = stuff.strip().split('\n')
            if trim == 1:
                lines = list(map(str.strip, lines))
        elif isinstance(stuff, list):
            lines = stuff
        elif isinstance(stuff, tuple):
            lines = stuff
        else:
            assert False, "argument to textlines must be string, list or tuple"

        for line in lines:
            escaped_text = self._canvas._escape(line)
            self._code.append('(%s) Tj T*' % escaped_text)
            if self._canvas.bottomup:
                self._y = self._y - self._leading
            else:
                self._y = self._y + self._leading
        self._x = self._x0
