# piddlePIL.py -- a Python Imaging Library backend for PIDDLE
# Copyright (C) 1999  Joseph J. Strout
# 
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""pidPIL

This module implements a Python Imaging Library PIDDLE canvas.
In other words, this is a PIDDLE backend that renders into a
PIL Image object.  From there, you can save as GIF, plot into
another PIDDLE canvas, etc.
		
Joe Strout (joe@strout.net), 10/26/99
modified for use with sping.
This requires Imaging to be installed as a package PIL
"""

###	 6/22/99: updated drawString to handle non-integer x and y

from rdkit.sping.pid import *
try:
  import Image, ImageFont, ImageDraw
except ImportError:
  from PIL import Image, ImageFont, ImageDraw
import math

from rdkit import six
import os
if __name__ == '__main__':
  _fontprefix = os.path.join(os.curdir, 'pilfonts')
else:
  _fontprefix = os.path.join(os.path.split(__file__)[0],'pilfonts')

# load font metrics
try:
  f = open(os.path.join(_fontprefix,'metrics.dat'), 'rb')
  from rdkit.six.moves import cPickle
  _widthmaps = cPickle.load(f)
  _ascents = cPickle.load(f)
  _descents = cPickle.load(f)
  f.close()
except Exception:
  print("Warning: unable to load font metrics from dir {0}".format(_fontprefix))
  _widthmaps = {}
  _ascents = {}
  _descents = {}
#finally:
#	pass	# (just here so we can comment out the except clause for debugging)

def _closestSize(size):
  supported = [8,10,12,14,18,24]		# list of supported sizes
  if size in supported: return size
  best = supported[0]
  bestdist = abs(size-best)
  for trial in supported[1:]:
    dist = abs(size - trial)
    if dist < bestdist:
      best = trial
      bestdist = dist
  return best

def _pilFontPath(face,size,bold=0):
  if face == 'monospaced': face = 'courier'
  elif face == 'serif': face = 'times'
  elif face == 'sansserif' or face == 'system': face = 'helvetica'

  if bold and face != 'symbol': fname = "%s-bold-%d.pil" % (face,size)
  else: fname = "%s-%d.pil" % (face,size)
  path = os.path.join(_fontprefix,fname)
  return path

def _matchingFontPath(font):
  # returns a font path which matches info in our font metrics
  if font.face: face = font.face
  else: face = 'times'

  size = _closestSize(font.size)
  if isinstance(face,six.string_types):
    path = _pilFontPath(face,size,font.bold)
    path = path.split(os.sep)[-1]
    if path in _widthmaps.keys(): return path
  else:
    for item in font.face:
      path = _pilFontPath(item,size,font.bold)
      path = path.split(os.sep)[-1]
      if path in _widthmaps.keys(): return path
  # not found?  Try it with courier, which should always be there
  path = _pilFontPath('courier',size,font.bold)
  return path.split(os.sep)[-1]

def _pilFont(font):
  if font.face: face = font.face
  else: face = 'times'

  size = _closestSize(font.size)
  if isinstance(face,six.string_types):
    try: 
      pilfont = ImageFont.load_path(_pilFontPath(face,size,font.bold))
    except Exception:
      return 0		# font not found!
  else:
    for item in font.face:
      pilfont = None
      try:
        pilfont = ImageFont.load_path(_pilFontPath(item,size,font.bold))
        break
      except Exception: pass
    if pilfont == None: return 0	# font not found!
  return pilfont

class PILCanvas( Canvas ):
  def __init__(self, size=(300,300), name='piddlePIL'):
    self._image = Image.new('RGB',(int(size[0]),int(size[1])), (255,255,255))
    self._pen = ImageDraw.ImageDraw(self._image)
    self._setFont( Font() )
    Canvas.__init__(self, size, name)

  def __setattr__(self, attribute, value):
    self.__dict__[attribute] = value
    if attribute == "defaultLineColor":
      self._setColor(self.defaultLineColor)

  # utility functions
  def _setColor(self,c):
    "Set the pen color from a piddle color."
    self._color=(int(c.red*255), int(c.green*255), int(c.blue*255))

  def _setFont(self,font):
    self._font=_pilFont(font)

  # public functions

  def getImage(self):
    return self._image
	
  def save(self, file=None, format=None):
    """format may be a string specifying a file extension corresponding to
    an image file format. Ex: 'png', 'jpeg', 'gif', 'tif' etc.
    These are defined by PIL, not by us so you need to check the docs.
    In general, I just specify an extension and let format default to None"""
    file = file or self.name
    if hasattr(file, 'write'):
      raise ValueError('fileobj not implemented for piddlePIL')
    # below here, file is guaranteed to be a string
    if format == None:
      if '.' not in file:
        filename = file + '.png' # default to producing jpg
      else:
        filename = file
        # format = file[-3:] # os.path.splitext(..)
    else:
      filename = file + '.' + format

    self._image.save(filename, format=format)


  def clear(self) :
    # why is edgeColor yellow ???
    self.drawRect( 0,0,self.size[0],self.size[1], edgeColor=yellow,fillColor=white )
    ### FIXME: need to reset canvas as well to defaults ???


  #------------ 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.defaultFont)."

    if not font: font = self.defaultFont
    if not _widthmaps: return font.size * len(s)

    path = _matchingFontPath(font)
    map = _widthmaps[path]
    out = 0
    for c in s:
      out += map.get(c,font.size)
    return out			

  def fontAscent(self, font=None):
    "Find the ascent (height above base) of the given font."

    if not font: font = self.defaultFont
    if not _ascents: return font.size

    path = _matchingFontPath(font)
    return _ascents[path]

  def fontDescent(self, font=None):
    "Find the descent (extent below base) of the given font."

    if not font: font = self.defaultFont
    if not _descents: return font.size/2

    path = _matchingFontPath(font)
    return _descents[path]

  #------------- drawing methods --------------
  def drawLine(self, x1,y1, x2,y2, color=None, width=None, 
               dash=None,**kwargs):
    "Draw a straight line between x1,y1 and x2,y2."
    # set color...
    if width is None:
      w = self.defaultLineWidth
    elif width:
      w = width
    else:
      return

    if color:
      if color == transparent: return
      self._setColor(color)
    elif self.defaultLineColor == transparent: return

    if not dash:
      self._pen.line( (x1,y1,x2,y2),fill=self._color,width=w )
    else:
      dx = x2-x1
      dy = y2-y1
      lineLen = math.sqrt(dx*dx+dy*dy)
      theta = math.atan2(dy,dx)
      cosT = math.cos(theta)
      sinT = math.sin(theta)

      pos = (x1,y1)
      dist = 0
      currDash = 0
      dashOn = 1
      while dist < lineLen:
        currL = dash[currDash%len(dash)]
        if(dist+currL > lineLen): currL = lineLen-dist
        endP = (pos[0] + currL*cosT, pos[1] + currL*sinT)
        if dashOn:
          self.drawLine(pos[0],pos[1],endP[0],endP[1],
                        color=color,width=width,dash=None,**kwargs)
        pos = endP
        dist += currL
        currDash += 1
        dashOn = not dashOn
        

        
  def drawPolygon(self, pointlist, 
                  edgeColor=None, edgeWidth=None, fillColor=None,
                  closed=0, dash=None, **kwargs):
    """drawPolygon(pointlist) -- draws a polygon
    pointlist: a list of (x,y) tuples defining vertices
    """
    # PIL's routine requires a sequence of tuples...
    # the input is not so restricted, so fix it
    pts = list(pointlist)
    for i in range(len(pts)):
      pts[i] = tuple(pts[i])

    # set color for fill...
    filling = 0
    if fillColor:
      if fillColor != transparent:
        self._setColor(fillColor)
        filling = 1
    elif self.defaultFillColor != transparent:
      self._setColor(self.defaultFillColor)
      filling = 1

    # do the fill
    if filling:
      pts = [(int(x[0]),int(x[1])) for x in pts]
      self._pen.polygon(pts,fill=self._color)

    # set edge width...
    if edgeWidth is None:
      edgeWidth = self.defaultLineWidth
    elif not edgeWidth:
      return

    # set color for edge...
    if edgeColor:
      self._setColor(edgeColor)
    else:
      self._setColor(self.defaultLineColor)


    # draw the outline
    if (closed or (pts[0][0]==pts[-1][0] and pts[0][1]==pts[-1][1])) \
                     and edgeWidth <= 1:
      self._pen.polygon(pts,outline=self._color)
    else:
      # ...since PIL's polygon routine insists on closing,
      # and does not support thick edges, we'll use our drawLine instead
      # OFI: use default color/width to speed this up!
      oldp = pts[0]
      if closed: pts.append(oldp)
      for p in pts[1:]:
        self.drawLine(oldp[0],oldp[1], p[0],p[1],
                      edgeColor, edgeWidth,dash=dash,
                      **kwargs)
        oldp = p

  def drawString(self, s, x,y, font=None, color=None, angle=0, **kwargs):
    "Draw a string starting at location x,y."
    x = int(x)
    y = int(y)
    if '\n' in s or '\r' in s:
      self.drawMultiLineString(s, x,y, font, color, angle,
                               **kwargs)
      return
    if not font: font = self.defaultFont

    if not color:
      color = self.defaultLineColor
    if color == transparent: return

    # draw into an offscreen Image
    # tmpsize was originally 1.2* stringWidth, added code to give enough room for single character strings (piddle bug#121995)
    sHeight = (self.fontAscent(font) + self.fontDescent(font))
    sWidth = self.stringWidth(s, font)
    tempsize = max(sWidth*1.2, sHeight*2.0)
    tempimg = Image.new('RGB',(int(tempsize),int(tempsize)), (0,0,0))

    temppen = ImageDraw.ImageDraw(tempimg)

    pilfont = _pilFont(font)
    if not pilfont: raise ValueError("bad font: %s"%font)
    pos = [4, int(tempsize/2 - self.fontAscent(font)) - self.fontDescent(font)]
    temppen.text( pos, s,font=pilfont,fill=(255,255,255))
    pos[1] = int(tempsize/2)

    if font.underline:
      ydown = (0.5 * self.fontDescent(font) )
      # thickness = 0.08 * font.size # may need to ceil this
      temppen.line((pos[0], pos[1]+ydown,pos[0]+sWidth,pos[1]+ydown))

    # rotate
    if angle:
      from math import pi, sin, cos
      tempimg = tempimg.rotate( angle, Image.BILINEAR )
      temppen = ImageDraw.ImageDraw(tempimg)
      radians = -angle * pi/180.0
      r = tempsize/2 - pos[0]
      pos[0] = int(tempsize/2 - r * cos(radians))
      pos[1] = int(pos[1] - r * sin(radians))

    ###temppen.rectangle( (pos[0],pos[1],pos[0]+2,pos[1]+2) ) # PATCH for debugging
    # colorize, and copy it in
    mask = tempimg.convert('L').point(lambda c:c)
    clr=(int(color.red*255), int(color.green*255), int(color.blue*255))
    temppen.rectangle( (0,0,tempsize,tempsize),fill=clr )
    self._image.paste( tempimg, (int(x)-pos[0],int(y)-pos[1]), mask )		



  def drawImage(self, image, x1,y1, x2=None,y2=None,**kwargs):
    """Draw a PIL Image into the specified rectangle.  If x2 and y2 are
    omitted, they are calculated from the image size."""

    if x2 and y2:
      bbox = image.getbbox()
      if x2-x1 != bbox[2]-bbox[0] or y2-y1 != bbox[3]-bbox[1]:
        image = image.resize( (x2-x1,y2-y1) )
    self._image.paste( image, (x1,y1) )

def test():
#... for testing...
	canvas = PILCanvas()

	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 )
	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, angle=45)
	
	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 )

	# now, for testing, save the image as a PNG file
	canvas.flush()
	canvas.getImage().save("test.png")

	
	return canvas

def testit(canvas, s, x,y, font=None):
	canvas.defaultLineColor = black
	canvas.drawString(s, x,y, font=font)
	canvas.defaultLineColor = blue
	w = canvas.stringWidth(s, font=font)
	canvas.drawLine(x,y, x+w,y)
	canvas.drawLine(x,y-canvas.fontAscent(font=font), x+w,y-canvas.fontAscent(font=font))
	canvas.drawLine(x,y+canvas.fontDescent(font=font), x+w,y+canvas.fontDescent(font=font))	
	
def test2():
	
	canvas = PILCanvas()
	testit( canvas, "Foogar", 20, 30 )
	
	testit( canvas, "Foogar", 20, 90, font=Font(size=24) )
	global dammit
	dammit = _pilFont(Font(size=24))
	
	testit( canvas, "Foogar", 20, 150, font=Font(face='courier',size=24) )
	
	testit( canvas, "Foogar", 20, 240, font=Font(face='courier') )
	
	
	import piddleQD
	global qdcanvas
	try:
		qdcanvas.close()
	except Exception: pass
	qdcanvas = piddleQD.QDCanvas()
	qdcanvas.drawImage( canvas.getImage(), 0, 0 );
	

if __name__ == '__main__': test()
	
