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