# piddlePDF.py - a PDF backend for PIDDLE
"""This is the PIDDLE back end for PDF.  It acts as a wrapper
over the pdfgen.Canvas class, and translates between the
PIDDLE graphics state and the PDF/PostScript one. It only
exposes PIDDLE methods; however, it has an attribute
self.pdf which offers numerous lower-level drawing routines.
"""

# Copyright Robinson Analytics 1998-9
# 6/10/99 - near total rewrite.  This is just a wrapper
# on top of a lower-level graphics canvas which speaks the
# Postscript metaphor.  Offers native methods for everything
# except drawFigure, which doesn't behave like PostScript
# paths so I left it unchanged.


#standard python library modules
import string
import cStringIO
import pdfmetrics
import glob
import os
import types
from math import sin, cos, pi, ceil

# app specific
from rdkit.sping import pagesizes
from rdkit.sping.pid import *
import pdfgen
import pdfgeom

#edit this is the setting offends you, or set it in the constructor
DEFAULT_PAGE_SIZE = pagesizes.A4
#DEFAULT_PAGE_SIZE = pagesizes.letter



##########################################################################
#
#      Font Mappings
# The brute force approach to finding the correct postscript font name;
# much safer than the rule-based ones we tried.
# preprocessor to reduce font face names to the shortest list
# possible.  Add any aliases you wish; it keeps looking up
# until it finds no more translations to do.  Any input
# will be lowercased before checking.
font_face_map = {
            'serif':'times',
            'sansserif':'helvetica',
            'monospaced':'courier',
            'arial':'helvetica'
            }

#maps a piddle font to a postscript one.
ps_font_map = {
            #face, bold, italic -> ps name
            ('times', 0, 0) :'Times-Roman', 
            ('times', 1, 0) :'Times-Bold', 
            ('times', 0, 1) :'Times-Italic', 
            ('times', 1, 1) :'Times-BoldItalic', 

            ('courier', 0, 0) :'Courier', 
            ('courier', 1, 0) :'Courier-Bold', 
            ('courier', 0, 1) :'Courier-Oblique', 
            ('courier', 1, 1) :'Courier-BoldOblique', 
            
            ('helvetica', 0, 0) :'Helvetica', 
            ('helvetica', 1, 0) :'Helvetica-Bold', 
            ('helvetica', 0, 1) :'Helvetica-Oblique', 
            ('helvetica', 1, 1) :'Helvetica-BoldOblique', 

            # there is only one Symbol font            
            ('symbol', 0, 0) :'Symbol', 
            ('symbol', 1, 0) :'Symbol', 
            ('symbol', 0, 1) :'Symbol', 
            ('symbol', 1, 1) :'Symbol', 

            # ditto for dingbats
            ('zapfdingbats', 0, 0) :'ZapfDingbats', 
            ('zapfdingbats', 1, 0) :'ZapfDingbats', 
            ('zapfdingbats', 0, 1) :'ZapfDingbats', 
            ('zapfdingbats', 1, 1) :'ZapfDingbats', 
      }
            
######################################################################
#
#     Canvas class
#
######################################################################

class PDFCanvas(Canvas):
    """This works by accumulating a list of strings containing 
    PDF page marking operators, as you call its methods.  We could
    use a big string but this is more efficient - only concatenate
    it once, with control over line ends.  When
    done, it hands off the stream to a PDFPage object."""

    def __init__(self,
            size=None,
            name="pidPDF.pdf",
            pagesize=DEFAULT_PAGE_SIZE
            ):
        #if no extension, add .PDF
        root, ext = os.path.splitext(name)
        if ext == '':
            name = root + '.pdf'

        #create the underlying pdfgen canvas and set some attributes
        self.pdf = pdfgen.Canvas(name, pagesize=pagesize, bottomup=0)  # add pagesize to constructor
        # by default do not use comrpression (mod by cwl, may not be necessary w/ newer pdfgen)
        self.pdf.setPageCompression(0)
        
        self.pdf.setLineCap(2)
        #now call super init, which will trigger
        #calls into self.pdf
        
        Canvas.__init__(self, size=size,name=name)

        #memorize stuff
        self.pagesize = pagesize
        self.filename = name
               
        # self.pdf.setPageSize(pagesize) # This doesn't seem to work correctly -cwl 
        if size == None:
            #take the page size, which might not be default
            self.drawingsize = self.pagesize
        else:
            #convenience for other platformslike GUI views
            #we let them centre a smaller drawing in a page            
            self.drawingsize = size  

        self.pageTransitionString = ''
        self.pageNumber = 1   # keep a count
               
        #if they specified a size smaller than page,
        # be helpful and centre their diagram
        if self.pagesize <> self.drawingsize:
            dx = 0.5 * (self.pagesize[0] - self.drawingsize[0])
            dy = 0.5 * (self.pagesize[1] - self.drawingsize[1])
            self.pdf.translate(dx, dy)

        
    def _resetDefaults(self):
        """Only used in setup - persist from page to page"""
        self.defaultLineColor = black
        self.defaultFillColor = transparent
        self.defaultLineWidth = 1
        self.defaultFont = Font()
        self.pdf.setLineCap(2)
        
        
    def showPage(self):
        """ensure basic settings are the same after a page break"""
        self.pdf.showPage()
        self.defaultFont = self.defaultFont
        self.defaultLineColor = self.defaultLineColor
        self.defaultFillColor = self.defaultFillColor
        self.defaultLineWidth = self.defaultLineWidth
        self.pdf.setLineCap(2)

        
    #------------ canvas capabilities -------------
    def isInteractive(self):
        return 0

    def canUpdate(self):
        return 0

    #------------ general management -------------
    def clear(self):
        "Not wll defined for file formats, use same as ShowPage"
        self.showPage()

    def flush(self):
        pass

    def save(self, file=None, format=None):
        """Saves the file.  If holding data, do
        a showPage() to save them having to.""" 

        if self.pdf.pageHasData():
            self.pdf.showPage()
            
        if hasattr(file, 'write'):
            self.pdf.save(fileobj=file)
        elif isinstance(file, types.StringType):
            self.pdf.save(filename=file)
        else:
            self.pdf.save()



    def setInfoLine(self, s):
        self.pdf.setTitle(s)


    #-------------handle assignment to defaultXXX-------

    def __setattr__(self, key, value):
        #we let it happen...
        self.__dict__[key] = value
        #...but take action if needed
        if key == "defaultLineColor":
            self._updateLineColor(value)
        elif key == "defaultLineWidth":
            self._updateLineWidth(value)
        elif key == "defaultFillColor":
            self._updateFillColor(value)
        elif key == "defaultFont":
            self._updateFont(value)
            
        
        
    def _updateLineColor(self, color):
        """Triggered when someone assigns to defaultLineColor"""
        self.pdf.setStrokeColorRGB(color.red, color.green, color.blue)
        
    def _updateFillColor(self, color):
        """Triggered when someone assigns to defaultFillColor"""
        self.pdf.setFillColorRGB(color.red, color.green, color.blue)
        
    def _updateLineWidth(self, width):
        """Triggered when someone assigns to defaultLineWidth"""
        self.pdf.setLineWidth(width)
        
    def _updateFont(self, font):
        """Triggered when someone assigns to defaultFont"""
        psfont = self._findPostScriptFontName(font)
        self.pdf.setFont(psfont, font.size)
        
    def _findPostScriptFontName(self, font):
        """Attempts to return proper font name."""

        #step 1 - no face ends up serif, others are lowercased
        if not font.face:
            face = 'serif'
        else:
            face = string.lower(font.face)
        while font_face_map.has_key(face):
            face = font_face_map[face]
        #step 2, - resolve bold/italic to get the right PS font name
        psname = ps_font_map[(face, font.bold, font.italic)]
        return psname
    


    
    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 = string.replace(s, '(','\(')
        s = string.replace(s, ')','\)')
        return s

    def resetDefaults(self):
        """If you drop down to a lower level, PIDDLE can lose
        track of the current graphics state.  Calling this after
        wards ensures that the canvas is updated to the same
        defaults as PIDDLE thinks they should be."""
        self.defaultFont = self.defaultFont
        self.defaultLineColor = self.defaultLineColor
        self.defaultFillColor = self.defaultFillColor
        self.defaultLineWidth = self.defaultLineWidth
    #------------ string/font info ------------
        
    def stringWidth(self, s, font=None):
        "Return the logical width of the string if it were drawn \
        in the current font (defaults to self.font)."
        if not font:
            font = self.defaultFont
        fontname = self._findPostScriptFontName(font)
        return pdfmetrics.stringwidth(s, fontname) * font.size * 0.001


    def fontHeight(self, font=None):
        if not font:
            font = self.defaultFont
        return font.size

    def fontAscent(self, font=None):
        if not font:
            font = self.defaultFont
        fontname = self._findPostScriptFontName(font)
        return pdfmetrics.ascent_descent[fontname][0] * 0.001 * font.size
        
    def fontDescent(self, font=None):
        if not font:
            font = self.defaultFont
        fontname = self._findPostScriptFontName(font)
        return -pdfmetrics.ascent_descent[fontname][1] * 0.001 * font.size

    #------------- drawing helpers --------------
    def _endPath(self, path, edgeColor, fillColor):
        """in PIDDLE, the edge and fil colors might be transparent,
        and might also be None, in which case they should be taken
        from the defaults.  This leads to a standard 10 lines of code
        when closing each shape, which are wrapped up here.  Use
        these if you implement new PIDDLE shapes."""
        #allow for transparent fills and lines
        fill = fillColor or self.defaultFillColor
        edge = edgeColor or self.defaultLineColor
        if (fill == transparent and edge == transparent):
            pass
        else:
            self.pdf.drawPath(
                        path,
                        (edge <> transparent),  #whether to stroke
                        (fill <> transparent)   #whether to fill
                        )
        
    #------------- drawing methods --------------

   
    def drawLine(self, x1,y1, x2,y2, color=None, width=None,
                 dash=None, **kwargs):
        """Calls the underlying methods in pdfgen.canvas.  For the
        highest performance, use canvas.setDefaultFont and
        canvas.setLineWidth, and draw batches of similar
        lines together."""
        #set the state if needed
        if color:
            self._updateLineColor(color)
        if width:
            self._updateLineWidth(width)

        # now do the work
        self.pdf.line(x1, y1, x2, y2)

        #now reset state if needed
        if color:
            self._updateLineColor(self.defaultLineColor)
        if width:
            self._updateLineWidth(self.defaultLineWidth)
        

    def drawLines(self, lineList, color=None, width=None,
                  dash=None, **kwargs):
        """Draws several distinct lines, all with same color
        and width, efficiently"""
        if color:
            self._updateLineColor(color)
        if width:
            self._updateLineWidth(width)

        self.pdf.lines(lineList)
        
        if color:
            self._updateLineColor(self.defaultLineColor)
        if width:
            self._updateLineWidth(self.defaultLineWidth)
        
    
    def drawString(self, s, x, y, font=None, color=None, angle=0,
                   **kwargs):
        """As it says, but many options to process.  It translates
        user space rather than text space, in case underlining is
        needed on rotated text.  It cheats and does literals
        for efficiency, avoiding changing the python graphics state."""
        self.pdf.addLiteral('%begin drawString')
        col = color or self.defaultLineColor
        if col != transparent:
            if '\n' in s or '\r' in s:
                #normalize line ends
                s = string.replace(s, '\r\n','\n')
                s = string.replace(s, '\n\r','\n')
                lines = string.split(s, '\n')
            else:
                lines = [s]
            fnt = font or self.defaultFont
            self._updateFont(fnt)
            text = self.pdf._escape(s)   

            # start of Chris's hacking
            # inserting basic commands here  to see if can get working
            textobj = self.pdf.beginText()

            if col <> self.defaultFillColor:
                textobj.setFillColorRGB(col.red,col.green, col.blue)

            if angle != 0 :
                co = cos(angle * pi / 180.0) 
                si = sin(angle * pi / 180.0)
                textobj.setTextTransform(co, -si, si, co, x, y)  #top down coords so reverse angle
            else :
                textobj.setTextOrigin(x,y)

            for line in lines:
                #keep underlining separate - it is slow and unusual anyway
                if fnt.underline:
                    #breaks on angled text - FIXME
                    ycursor = textobj.getY() # returns offset from last set origin 
                    dy = 0.5 * self.fontDescent(fnt)
                    width = self.stringWidth(line, fnt)
                    linewidth = fnt.size * 0.1

                    self.pdf.saveState()
                    self.pdf.setLineWidth(linewidth)
                    self.pdf.translate(x,y) # need to translate first before rotate
                    if angle != 0 :
                        self.pdf.rotate(-angle)
                    self.pdf.translate(0, ycursor-y) #move down to start of current text line 
                    self.pdf.line(0,dy, width, dy) 
                    self.pdf.restoreState()
                    lasty = ycursor
                textobj.textLine(line) # adds text to textobj, advances getY's cursor
            # finally actually send text object to the page
            self.pdf.drawText(textobj)  # draw all the text afterwards? Doesn't seem right
        self.pdf.addLiteral('%end drawString')
        # done wth drawString()
        
    def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4,
                  edgeColor=None, edgeWidth=None, fillColor=None, closed=0,
                  dash=None, **kwargs):
        """This could do two totally different things.  If not closed,
        just does a bezier curve so fill is irrelevant.  If closed,
        it is actually a filled shape."""
        if closed:
            if edgeColor:
                self._updateLineColor(edgeColor)
            if edgeWidth:
                self._updateLineWidth(edgeWidth)
            if fillColor:
                self._updateFillColor(fillColor)

            p = self.pdf.beginPath()
            p.moveTo(x1, y1)
            p.curveTo(x2, y2, x3, y3, x4, y4)
            p.close()
            self._endPath(p, edgeColor, fillColor) #handles case of transparency
            
            if edgeColor:
                self._updateLineColor(self.defaultLineColor)
            if edgeWidth:
                self._updateLineWidth(self.defaultLineWidth)
            if fillColor:
                self._updateFillColor(self.defaultFillColor)
        else:
            #just a plain old line segment
            if edgeColor:
                self._updateLineColor(edgeColor)
            if edgeWidth:
                self._updateLineWidth(edgeWidth)
            
            self.pdf.bezier(x1, y1, x2, y2, x3, y3, x4, y4)

            if edgeColor:
                self._updateLineColor(self.defaultLineColor)
            if edgeWidth:
                self._updateLineWidth(self.defaultLineWidth)
            
    def drawRect(self, x1, y1, x2, y2, edgeColor=None, 
            edgeWidth=None, fillColor=None,dash=None,**kwargs):
        if edgeColor:
            self._updateLineColor(edgeColor)
        if edgeWidth:
            self._updateLineWidth(edgeWidth)
        if fillColor:
            self._updateFillColor(fillColor)

        p = self.pdf.beginPath()
        p.rect(x1, y1, x2-x1, y2-y1)
        self._endPath(p, edgeColor, fillColor) #handles case of transparency
        
        if edgeColor:
            self._updateLineColor(self.defaultLineColor)
        if edgeWidth:
            self._updateLineWidth(self.defaultLineWidth)
        if fillColor:
            self._updateFillColor(self.defaultFillColor)
    
    #drawRoundRect is inherited - cannot really improve on that one,
    #and figures are quite efficient now.
    def drawEllipse(self, x1,y1, x2,y2, edgeColor=None, edgeWidth=None,
                    fillColor=None, dash=None, **kwargs):
        if edgeColor:
            self._updateLineColor(edgeColor)
        if edgeWidth:
            self._updateLineWidth(edgeWidth)
        if fillColor:
            self._updateFillColor(fillColor)

        p = self.pdf.beginPath()
        p.ellipse(x1, y1, x2-x1, y2-y1)
        self._endPath(p, edgeColor, fillColor) #handles case of transparency
        
        if edgeColor:
            self._updateLineColor(self.defaultLineColor)
        if edgeWidth:
            self._updateLineWidth(self.defaultLineWidth)
        if fillColor:
            self._updateFillColor(self.defaultFillColor)
    
    def drawArc(self, x1,y1, x2,y2, startAng=0, extent=90, edgeColor=None,
                edgeWidth=None, fillColor=None, dash=None, **kwargs):
        """This draws a PacMan-type shape connected to the centre.  One
        idiosyncracy - if you specify an edge color, it apples to the
        outer curved rim but not the radial edges."""
        if edgeColor:
            self._updateLineColor(edgeColor)
        if edgeWidth:
            self._updateLineWidth(edgeWidth)
        if fillColor:
            self._updateFillColor(fillColor)
        # I need to do some more work on flipping the coordinate system -
        # in pdfgen - note the angle reversal needed when drawing top-down.
        pointList = pdfgeom.bezierArc(x1,y1, x2,y2, -startAng, -extent)
        start = pointList[0]
        end = pointList[-1]
        x_cen = 0.5 * (x1 + x2)
        y_cen = 0.5 * (y1 + y2)

        #first the fill                
        p = self.pdf.beginPath()
        p.moveTo(x_cen, y_cen)
        p.lineTo(start[0], start[1])
        for curve in pointList:
            p.curveTo(curve[2], curve[3], curve[4], curve[5], curve[6], curve[7])
        p.close()  #back to centre
        self._endPath(p, transparent, fillColor) #handles case of transparency
        #now the outer rim
        p2 = self.pdf.beginPath()
        p2.moveTo(start[0], start[1])
        for curve in pointList:
            p2.curveTo(curve[2], curve[3], curve[4], curve[5], curve[6], curve[7])
        self._endPath(p2, edgeColor, transparent) #handles case of transparency
        
        
        if edgeColor:
            self._updateLineColor(self.defaultLineColor)
        if edgeWidth:
            self._updateLineWidth(self.defaultLineWidth)
        if fillColor:
            self._updateFillColor(self.defaultFillColor)
    

    def drawPolygon(self, pointlist, edgeColor=None, 
            edgeWidth=None, fillColor=None, closed=0, dash=None, **kwargs):
        """As it says.  Easy with paths!"""
        if edgeColor:
            self._updateLineColor(edgeColor)
        if edgeWidth:
            self._updateLineWidth(edgeWidth)
        if fillColor:
            self._updateFillColor(fillColor)

        p = self.pdf.beginPath()
        p.moveTo(pointlist[0][0], pointlist[0][1])
        for point in pointlist[1:]:
            p.lineTo(point[0], point[1])
        if closed:
            p.close()
        
        self._endPath(p, edgeColor, fillColor) #handles case of transparency
        
        if edgeColor:
            self._updateLineColor(self.defaultLineColor)
        if edgeWidth:
            self._updateLineWidth(self.defaultLineWidth)
        if fillColor:
            self._updateFillColor(self.defaultFillColor)

##    def drawFigure(self, partList,
##            edgeColor=None, edgeWidth=None, fillColor=None, closed=0):
##        """This is PIDDLE's attempt at Postscript paths.  Due to
##        necessary limitations in the algorithm, if the start and end
##        points are not connected but closed=1, then you get the full shape
##        filled but the final line segment does not join up.  I have to
##        do extra work to simulate this."""
##        
##        if edgeColor:
##            self._updateLineColor(edgeColor)
##        if edgeWidth:
##            self._updateLineWidth(edgeWidth)
##        if fillColor:
##            self._updateFillColor(fillColor)
##
##        p1 = self.pdf.beginPath()  #use for the fill (i.e. closed)
##        p2 = self.pdf.beginPath()  #use for the edge (may not be closed)
##
##        #move to first point
##        start = (partList[0][1:3])
##        end = None
##        p1.moveTo(start[0], start[1])
##        p2.moveTo(start[0], start[1])
##        
##        for tuple in partList:
##            op = tuple[0]
##            args = list(tuple[1:])
##            start = args[0:2]
##            # lineTo the start if not coincident with end of last segment
##            if start <> end:
##                p1.lineTo(start[0], start[1])
##                p2.lineTo(start[0], start[1])
##                
##            #now draw appropriate segment           
##            if op == figureLine:
##                p1.lineTo(args[2], args[3])
##                p2.lineTo(args[2], args[3])
##                end = args[2:4]
##            elif op == figureArc:
##                #p1.arcTo(args[0], args[1], args[2], args[3], args[4], args[5])
##                #p2.arcTo(args[0], args[1], args[2], args[3], args[4], args[5])
##                p1.arc(args[0], args[1], args[2], args[3], args[4], args[5])
##                p2.arc(args[0], args[1], args[2], args[3], args[4], args[5])
##                end = args[2:4]
##            elif op == figureCurve:
##                p1.curveTo(args[2], args[3], args[4], args[5], args[6], args[7])
##                p2.curveTo(args[2], args[3], args[4], args[5], args[6], args[7])
##                end = args[6:8]
##            else:
##                raise TypeError, "unknown figure operator: " + op
##
##        #now for the weirdness
##        p1.close()
##        if closed:
##            p2.close()
##            print 'closed edge path'
##        print 'inner path p1:' + p1.getCode()
##        print 'outer path p2:' + p2.getCode()
##        
##        self._endPath(p1, transparent, fillColor)
##        self._endPath(p2, edgeColor, transparent)
##        
##        if edgeColor:
##            self._updateLineColor(self.defaultLineColor)
##        if edgeWidth:
##            self._updateLineWidth(self.defaultLineWidth)
##        if fillColor:
##            self._updateFillColor(self.defaultFillColor)

        
    def drawImage(self, image, x1,y1, x2=None,y2=None,**kwargs):
        """Draw a PIL Image or image filename into the specified rectangle.
        If x2 and y2 are omitted, they are calculated from the image size.
        """
        # chris starts meddling here -cwl
        # piddle only takes PIL images 
        im_width, im_height = image.size
        if not x2 :
            x2 = x1 + im_width
        if not y2 :
            y2 = y1 + im_height
            
        self.pdf.saveState()  # I'm changing coordinates to isolate the problem -cwl

        self.pdf.translate(x1,y1)
        self.pdf.drawInlineImage(image, 0, 0, abs(x1-x2), abs(y1-y2))

        self.pdf.restoreState()
        
        ### original code below -cwl
        #the underlying canvas uses a bott-up coord system, so flips things        
        #if x2:
            #width = abs(x2 - x1)
            #x = min(x1, x2)
        #if y2:
            #height = abs(y2 - y1)
            #y = min(y1, y2)
        #self.pdf.drawInlineImage(image, x, y, width, height)        


##########################################################
#
#   non-standard extensions outside Piddle API
#
##########################################################


    def drawLiteral(self, literal):
        #adds a chunk of raw stuff to the PDF contents stream
        self.code.append(literal)




def test():
  #... for testing...
  canvas = PDFCanvas(name="test")

  canvas.defaultLineColor = Color(0.7,0.7,1.0)	# light blue
  canvas.drawLines( map(lambda i:(i*10,0,i*10,300), range(30)) )
  canvas.drawLines( map(lambda i:(0,i*10,300,i*10), range(30)) )
  canvas.defaultLineColor = black		

  canvas.drawLine(10,200, 20,190, color=red)

  canvas.drawEllipse( 130,30, 200,100, fillColor=yellow, edgeWidth=4 )

  canvas.drawArc( 130,30, 200,100, 45,50, fillColor=blue, edgeColor=navy, edgeWidth=4 )

  canvas.defaultLineWidth = 4
  canvas.drawRoundRect( 30,30, 100,100, fillColor=blue, edgeColor=maroon,dash=(3,3) )
  canvas.drawCurve( 20,20, 100,50, 50,100, 160,160 )

  canvas.drawString("This is a test!", 30,130, Font(face="times",size=16,bold=1), 
                  color=green, angle=-45)

  canvas.drawString("This is a test!", 30,130, color=red)

  polypoints = [ (160,120), (130,190), (210,145), (110,145), (190,190) ]
  canvas.drawPolygon(polypoints, fillColor=lime, edgeColor=red, edgeWidth=3, closed=1)

  canvas.drawRect( 200,200,260,260, edgeColor=yellow, edgeWidth=5 )
  canvas.drawLine( 200,260,260,260, color=green, width=5 )
  canvas.drawLine( 260,200,260,260, color=red, width=5 )

  canvas.save('test.pdf')

if __name__ == '__main__':
  test()
  #dashtest()
  #test2()
