rdkit.Chem.Draw.cairoCanvas

Package rdkit :: Package Chem :: Package Draw :: Module cairoCanvas |
[hide private]
[frames] | no frames]

# Source Code for Module rdkit.Chem.Draw.cairoCanvas

      1  #
      2  #  Copyright (C) 2008 Greg Landrum
      3  #  Copyright (C) 2009 Uwe Hoffmann
      4  #
      5  #   @@ All Rights Reserved @@
      6  #  This file is part of the RDKit.
      7  #  The contents are covered by the terms of the BSD license
      8  #  which is included in the file license.txt, found at the root
      9  #  of the RDKit source tree.
     10  #
     11  import array
     12  import math
     13  import os
     14  import re
     15
     16  from PIL import Image
     17
     18  from rdkit import six
     19  from rdkit.Chem.Draw.canvasbase import CanvasBase
     20
     21  if not six.PY3:
     22    bytes = buffer
     23
     24  have_cairocffi = False
     25  # for Python3, import cairocffi preferably
     26  if six.PY3:
     27    try:
     28      import cairocffi as cairo
     29    except ImportError:
     30      import cairo
     31    else:
     32      have_cairocffi = True
     33  else:
     34    try:
     35      import cairo
     36    except ImportError:
     37      try:
     38        import cairocffi as cairo
     39      except:
     40        raise
     41      else:
     42        have_cairocffi = True
     43  have_pango = False
     44  if 'RDK_NOPANGO' not in os.environ:
     45    if have_cairocffi:
     46      import cffi
     47      import platform
     48      ffi = cffi.FFI()
     49      ffi.cdef('''
     50          /* GLib */
     51          typedef void* gpointer;
     52          typedef void cairo_t;
     53          typedef void PangoFontDescription;
     54          void g_object_unref (gpointer object);
     55
     56          /* Pango and PangoCairo */
     57          #define PANGO_SCALE 1024
     58          typedef ... PangoLayout;
     59          typedef enum {
     60              PANGO_ALIGN_LEFT,
     61              PANGO_ALIGN_CENTER,
     62              PANGO_ALIGN_RIGHT
     63          } PangoAlignment;
     64          typedef struct PangoRectangle {
     65            int x;
     66            int y;
     67            int width;
     68            int height;
     69          } PangoRectangle;
     70          PangoLayout *pango_cairo_create_layout (cairo_t *cr);
     71          void pango_cairo_update_layout (cairo_t *cr, PangoLayout *layout);
     72          void pango_cairo_show_layout (cairo_t *cr, PangoLayout *layout);
     73          void pango_layout_set_alignment (
     74              PangoLayout *layout, PangoAlignment alignment);
     75          void pango_layout_set_markup (
     76              PangoLayout *layout, const char *text, int length);
     77          void pango_layout_get_pixel_extents (PangoLayout *layout,
     78              PangoRectangle *ink_rect, PangoRectangle *logical_rect);
     79          PangoFontDescription *pango_font_description_new (void);
     80          void pango_font_description_free (PangoFontDescription *desc);
     81          void pango_font_description_set_family (PangoFontDescription *desc,
     82              const char *family);
     83          void pango_font_description_set_size (PangoFontDescription *desc,
     84              int size);
     85          void pango_layout_set_font_description (PangoLayout *layout,
     86              const PangoFontDescription *desc);
     87      ''')
     88      if platform.system() == 'Windows':
     89        defaultLibs = {
     90          'pango_default_lib': 'libpango-1.0-0.dll',
     91          'pangocairo_default_lib': 'libpangocairo-1.0-0.dll',
     92          'gobject_default_lib': 'libgobject-2.0-0.dll'
     93        }
     94      else:
     95        defaultLibs = {
     96          'pango_default_lib': 'pango-1.0',
     97          'pangocairo_default_lib': 'pangocairo-1.0',
     98          'gobject_default_lib': 'gobject-2.0'
     99        }
    100      import ctypes.util
    101      for libType in ['pango', 'pangocairo', 'gobject']:
    102        envVar = 'RDK_' + libType.upper() + '_LIB'
    103        envVarSet = False
    104        if envVar in os.environ:
    105          envVarSet = True
    106          libName = os.environ[envVar]
    107        else:
    108          libName = defaultLibs[libType + '_default_lib']
    109        libPath = ctypes.util.find_library(libName)
    110        exec(libType + ' = None')
    111        importError = False
    112        if libPath:
    113          try:
    114            exec(libType + ' = ffi.dlopen("' + libPath.replace('\\', '\\\\') + '")')
    115          except:
    116            if envVarSet:
    117              importError = True
    118            else:
    119              pass
    120        else:
    121          importError = True
    122        if importError:
    123          raise ImportError(envVar + ' set to ' + libName + ' but ' + libType.upper() +
    124                            ' library cannot be loaded.')
    125      have_pango = (pango and pangocairo and gobject)
    126    else:
    127      for libType in ['pango', 'pangocairo']:
    128        try:
    129          exec('import ' + libType)
    130        except ImportError:
    131          exec(libType + ' = None')
    132      have_pango = (pango and pangocairo)
    133
    134  if (not hasattr(cairo.ImageSurface, 'get_data') and
    135      not hasattr(cairo.ImageSurface, 'get_data_as_rgba')):
    136    raise ImportError('cairo version too old')
    137
    138  scriptPattern = re.compile(r'\<.+?\>')
    139
    140
    141 -class Canvas(CanvasBase):
    142
    143 -  def __init__(self,
    144                 image=None,  # PIL image
    145                 size=None,
    146                 ctx=None,
    147                 imageType=None,  # determines file type
    148                 fileName=None,  # if set determines output file name
    149                 ):
    150      """
    151      Canvas can be used in four modes:
    152      1) using the supplied PIL image
    153      2) using the supplied cairo context ctx
    154      3) writing to a file fileName with image type imageType
    155      4) creating a cairo surface and context within the constructor
    156      """
    157      self.image = None
    158      self.imageType = imageType
    159      if image is not None:
    160        try:
    161          imgd = getattr(image, 'tobytes', image.tostring)("raw", "BGRA")
    162        except SystemError:
    163          r, g, b, a = image.split()
    164          mrg = Image.merge("RGBA", (b, g, r, a))
    165          imgd = getattr(mrg, 'tobytes', mrg.tostring)("raw", "RGBA")
    166
    167        a = array.array('B', imgd)
    168        stride = image.size[0] * 4
    169        surface = cairo.ImageSurface.create_for_data(a, cairo.FORMAT_ARGB32, image.size[0],
    170                                                     image.size[1], stride)
    171        ctx = cairo.Context(surface)
    172        size = image.size[0], image.size[1]
    173        self.image = image
    174      elif ctx is None and size is not None:
    175        if hasattr(cairo, "PDFSurface") and imageType == "pdf":
    176          surface = cairo.PDFSurface(fileName, size[0], size[1])
    177        elif hasattr(cairo, "SVGSurface") and imageType == "svg":
    178          surface = cairo.SVGSurface(fileName, size[0], size[1])
    179        elif hasattr(cairo, "PSSurface") and imageType == "ps":
    180          surface = cairo.PSSurface(fileName, size[0], size[1])
    181        elif imageType == "png":
    182          surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1])
    183        else:
    184          raise ValueError("Unrecognized file type. Valid choices are pdf, svg, ps, and png")
    185        ctx = cairo.Context(surface)
    186        ctx.set_source_rgb(1, 1, 1)
    187        ctx.paint()
    188      else:
    189        surface = ctx.get_target()
    190        if size is None:
    191          try:
    192            size = surface.get_width(), surface.get_height()
    193          except AttributeError:
    194            size = None
    195      self.ctx = ctx
    196      self.size = size
    197      self.surface = surface
    198      self.fileName = fileName
    199
    200 -  def flush(self):
    201      """temporary interface, must be splitted to different methods,
    202      """
    203      if self.fileName and self.imageType == 'png':
    204        self.surface.write_to_png(self.fileName)
    205      elif self.image is not None:
    206        # on linux at least it seems like the PIL images are BGRA, not RGBA:
    207        if hasattr(self.surface, 'get_data'):
    208          getattr(self.image, 'frombytes', self.image.fromstring)(bytes(self.surface.get_data()),
    209                                                                  "raw", "BGRA", 0, 1)
    210        else:
    211          getattr(self.image, 'frombytes', self.image.fromstring)(
    212            bytes(self.surface.get_data_as_rgba()), "raw", "RGBA", 0, 1)
    213        self.surface.finish()
    214      elif self.imageType == "png":
    215        if hasattr(self.surface, 'get_data'):
    216          buffer = self.surface.get_data()
    217        else:
    218          buffer = self.surface.get_data_as_rgba()
    219        return buffer
    220
    221 -  def _doLine(self, p1, p2, **kwargs):
    222      if kwargs.get('dash', (0, 0)) == (0, 0):
    223        self.ctx.move_to(p1[0], p1[1])
    224        self.ctx.line_to(p2[0], p2[1])
    225      else:
    226        dash = kwargs['dash']
    227        pts = self._getLinePoints(p1, p2, dash)
    228
    229        currDash = 0
    230        dashOn = True
    231        while currDash < (len(pts) - 1):
    232          if dashOn:
    233            p1 = pts[currDash]
    234            p2 = pts[currDash + 1]
    235            self.ctx.move_to(p1[0], p1[1])
    236            self.ctx.line_to(p2[0], p2[1])
    237          currDash += 1
    238          dashOn = not dashOn
    239
    240 -  def addCanvasLine(self, p1, p2, color=(0, 0, 0), color2=None, **kwargs):
    241      self.ctx.set_line_width(kwargs.get('linewidth', 1))
    242      if color2 and color2 != color:
    243        mp = (p1[0] + p2[0]) / 2., (p1[1] + p2[1]) / 2.
    244        self.ctx.set_source_rgb(*color)
    245        self._doLine(p1, mp, **kwargs)
    246        self.ctx.stroke()
    247        self.ctx.set_source_rgb(*color2)
    248        self._doLine(mp, p2, **kwargs)
    249        self.ctx.stroke()
    250      else:
    251        self.ctx.set_source_rgb(*color)
    252        self._doLine(p1, p2, **kwargs)
    253        self.ctx.stroke()
    254
    255 -  def _addCanvasText1(self, text, pos, font, color=(0, 0, 0), **kwargs):
    256      if font.weight == 'bold':
    257        weight = cairo.FONT_WEIGHT_BOLD
    258      else:
    259        weight = cairo.FONT_WEIGHT_NORMAL
    260      self.ctx.select_font_face(font.face, cairo.FONT_SLANT_NORMAL, weight)
    261      text = scriptPattern.sub('', text)
    262      self.ctx.set_font_size(font.size)
    263      w, h = self.ctx.text_extents(text)[2:4]
    264      bw, bh = w + h * 0.4, h * 1.4
    265      offset = w * pos[2]
    266      dPos = pos[0] - w / 2. + offset, pos[1] + h / 2.
    267      self.ctx.set_source_rgb(*color)
    268      self.ctx.move_to(*dPos)
    269      self.ctx.show_text(text)
    270
    271      if 0:
    272        self.ctx.move_to(dPos[0], dPos[1])
    273        self.ctx.line_to(dPos[0] + bw, dPos[1])
    274        self.ctx.line_to(dPos[0] + bw, dPos[1] - bh)
    275        self.ctx.line_to(dPos[0], dPos[1] - bh)
    276        self.ctx.line_to(dPos[0], dPos[1])
    277        self.ctx.close_path()
    278        self.ctx.stroke()
    279
    280      return (bw, bh, offset)
    281
    282 -  def _addCanvasText2(self, text, pos, font, color=(0, 0, 0), **kwargs):
    283      if font.weight == 'bold':
    284        weight = cairo.FONT_WEIGHT_BOLD
    285      else:
    286        weight = cairo.FONT_WEIGHT_NORMAL
    287      self.ctx.select_font_face(font.face, cairo.FONT_SLANT_NORMAL, weight)
    288      orientation = kwargs.get('orientation', 'E')
    289
    290      plainText = scriptPattern.sub('', text)
    291
    292      # for whatever reason, the font size using pango is larger
    293      # than that w/ default cairo (at least for me)
    294      pangoCoeff = 0.8
    295
    296      if have_cairocffi:
    297        measureLout = pangocairo.pango_cairo_create_layout(self.ctx._pointer)
    298        pango.pango_layout_set_alignment(measureLout, pango.PANGO_ALIGN_LEFT)
    299        pango.pango_layout_set_markup(measureLout, plainText.encode('latin1'), -1)
    300        lout = pangocairo.pango_cairo_create_layout(self.ctx._pointer)
    301        pango.pango_layout_set_alignment(lout, pango.PANGO_ALIGN_LEFT)
    302        pango.pango_layout_set_markup(lout, text.encode('latin1'), -1)
    303        fnt = pango.pango_font_description_new()
    304        pango.pango_font_description_set_family(fnt, font.face.encode('latin1'))
    305        pango.pango_font_description_set_size(fnt,
    306                                              int(round(font.size * pango.PANGO_SCALE * pangoCoeff)))
    307        pango.pango_layout_set_font_description(lout, fnt)
    308        pango.pango_layout_set_font_description(measureLout, fnt)
    309        pango.pango_font_description_free(fnt)
    310      else:
    311        cctx = pangocairo.CairoContext(self.ctx)
    312        measureLout = cctx.create_layout()
    313        measureLout.set_alignment(pango.ALIGN_LEFT)
    314        measureLout.set_markup(plainText)
    315        lout = cctx.create_layout()
    316        lout.set_alignment(pango.ALIGN_LEFT)
    317        lout.set_markup(text)
    318        fnt = pango.FontDescription('%s %d' % (font.face, font.size * pangoCoeff))
    319        lout.set_font_description(fnt)
    320        measureLout.set_font_description(fnt)
    321
    322      # this is a bit kludgy, but empirically we end up with too much
    323      # vertical padding if we use the text box with super and subscripts
    324      # for the measurement.
    325      if have_cairocffi:
    326        iext = ffi.new('PangoRectangle *')
    327        lext = ffi.new('PangoRectangle *')
    328        iext2 = ffi.new('PangoRectangle *')
    329        lext2 = ffi.new('PangoRectangle *')
    330        pango.pango_layout_get_pixel_extents(measureLout, iext, lext)
    331        pango.pango_layout_get_pixel_extents(lout, iext2, lext2)
    332        w = lext2.width - lext2.x
    333        h = lext.height - lext.y
    334      else:
    335        iext, lext = measureLout.get_pixel_extents()
    336        iext2, lext2 = lout.get_pixel_extents()
    337        w = lext2[2] - lext2[0]
    338        h = lext[3] - lext[1]
    339      pad = [h * .2, h * .3]
    340      # another empirical correction: labels draw at the bottom
    341      # of bonds have too much vertical padding
    342      if orientation == 'S':
    343        pad[1] *= 0.5
    344      bw, bh = w + pad[0], h + pad[1]
    345      offset = w * pos[2]
    346      if 0:
    347        if orientation == 'W':
    348          dPos = pos[0] - w + offset, pos[1] - h / 2.
    349        elif orientation == 'E':
    350          dPos = pos[0] - w / 2 + offset, pos[1] - h / 2.
    351        else:
    352          dPos = pos[0] - w / 2 + offset, pos[1] - h / 2.
    353        self.ctx.move_to(dPos[0], dPos[1])
    354      else:
    355        dPos = pos[0] - w / 2. + offset, pos[1] - h / 2.
    356        self.ctx.move_to(dPos[0], dPos[1])
    357
    358      self.ctx.set_source_rgb(*color)
    359      if have_cairocffi:
    360        pangocairo.pango_cairo_update_layout(self.ctx._pointer, lout)
    361        pangocairo.pango_cairo_show_layout(self.ctx._pointer, lout)
    362        gobject.g_object_unref(lout)
    363        gobject.g_object_unref(measureLout)
    364      else:
    365        cctx.update_layout(lout)
    366        cctx.show_layout(lout)
    367
    368      if 0:
    369        self.ctx.move_to(dPos[0], dPos[1])
    370        self.ctx.line_to(dPos[0] + bw, dPos[1])
    371        self.ctx.line_to(dPos[0] + bw, dPos[1] + bh)
    372        self.ctx.line_to(dPos[0], dPos[1] + bh)
    373        self.ctx.line_to(dPos[0], dPos[1])
    374        self.ctx.close_path()
    375        self.ctx.stroke()
    376
    377      return (bw, bh, offset)
    378
    379 -  def addCanvasText(self, text, pos, font, color=(0, 0, 0), **kwargs):
    380      if have_pango:
    381        textSize = self._addCanvasText2(text, pos, font, color, **kwargs)
    382      else:
    383        textSize = self._addCanvasText1(text, pos, font, color, **kwargs)
    384      return textSize
    385
    386 -  def addCanvasPolygon(self, ps, color=(0, 0, 0), fill=True, stroke=False, **kwargs):
    387      if not fill and not stroke:
    388        return
    389      self.ctx.set_source_rgb(*color)
    390      self.ctx.move_to(ps[0][0], ps[0][1])
    391      for p in ps[1:]:
    392        self.ctx.line_to(p[0], p[1])
    393      self.ctx.close_path()
    394      if stroke:
    395        if fill:
    396          self.ctx.stroke_preserve()
    397        else:
    398          self.ctx.stroke()
    399      if fill:
    400        self.ctx.fill()
    401
    402 -  def addCanvasDashedWedge(self, p1, p2, p3, dash=(2, 2), color=(0, 0, 0), color2=None, **kwargs):
    403      self.ctx.set_line_width(kwargs.get('linewidth', 1))
    404      self.ctx.set_source_rgb(*color)
    405      dash = (3, 3)
    406      pts1 = self._getLinePoints(p1, p2, dash)
    407      pts2 = self._getLinePoints(p1, p3, dash)
    408
    409      if len(pts2) < len(pts1):
    410        pts2, pts1 = pts1, pts2
    411
    412      for i in range(len(pts1)):
    413        self.ctx.move_to(pts1[i][0], pts1[i][1])
    414        self.ctx.line_to(pts2[i][0], pts2[i][1])
    415      self.ctx.stroke()
    416
    417 -  def addCircle(self, center, radius, color=(0, 0, 0), fill=True, stroke=False, alpha=1.0,
    418                  **kwargs):
    419      if not fill and not stroke:
    420        return
    421      self.ctx.set_source_rgba(color[0], color[1], color[2], alpha)
    422      self.ctx.arc(center[0], center[1], radius, 0, 2. * math.pi)
    423      self.ctx.close_path()
    424      if stroke:
    425        if fill:
    426          self.ctx.stroke_preserve()
    427        else:
    428          self.ctx.stroke()
    429      if fill:
    430        self.ctx.fill()
    431