1
2
3
4
5
6
7
8
9
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
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
142
143 - def __init__(self,
144 image=None,
145 size=None,
146 ctx=None,
147 imageType=None,
148 fileName=None,
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
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
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
293
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
323
324
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
341
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