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