python-igraph manual

For using igraph from Python

   Home       Trees       Indices       Help   
Package igraph :: Package drawing :: Module text
[hide private]

Source Code for Module igraph.drawing.text

  1  """ 
  2  Drawers for labels on plots. 
  3   
  4  @undocumented: test 
  5  """ 
  6   
  7  import re 
  8   
  9  from igraph.compat import property 
 10  from igraph.drawing.baseclasses import AbstractCairoDrawer 
 11  from warnings import warn 
 12   
 13  __all__ = ["TextAlignment", "TextDrawer"] 
 14  __license__ = "GPL" 
 15   
 16  __docformat__ = "restructuredtext en" 
17 18 ##################################################################### 19 20 -class TextAlignment(object):
21 """Text alignment constants.""" 22 23 LEFT, CENTER, RIGHT = "left", "center", "right" 24 TOP, BOTTOM = "top", "bottom" 25
26 ##################################################################### 27 28 -class TextAlignment(object):
29 """Text alignment constants.""" 30 31 LEFT, CENTER, RIGHT = "left", "center", "right" 32 TOP, BOTTOM = "top", "bottom" 33
34 ##################################################################### 35 36 -class TextDrawer(AbstractCairoDrawer):
37 """Class that draws text on a Cairo context. 38 39 This class supports multi-line text unlike the original Cairo text 40 drawing methods.""" 41 42 LEFT, CENTER, RIGHT = "left", "center", "right" 43 TOP, BOTTOM = "top", "bottom" 44
45 - def __init__(self, context, text="", halign="center", valign="center"):
46 """Constructs a new instance that will draw the given `text` on 47 the given Cairo `context`.""" 48 super(TextDrawer, self).__init__(context, (0, 0)) 49 self.text = text 50 self.halign = halign 51 self.valign = valign 52
53 - def draw(self, wrap=False):
54 """Draws the text in the current bounding box of the drawer. 55 56 Since the class itself is an instance of `AbstractCairoDrawer`, it 57 has an attribute named ``bbox`` which will be used as a bounding 58 box. 59 60 :Parameters: 61 wrap : boolean 62 whether to allow re-wrapping of the text if it does not fit 63 within the bounding box horizontally. 64 """ 65 ctx = self.context 66 bbox = self.bbox 67 68 text_layout = self.get_text_layout(bbox.left, bbox.top, bbox.width, wrap) 69 if not text_layout: 70 return 71 72 _, font_descent, line_height = ctx.font_extents()[:3] 73 yb = ctx.text_extents(text_layout[0][2])[1] 74 total_height = len(text_layout) * line_height 75 76 if self.valign == self.BOTTOM: 77 # Bottom vertical alignment 78 dy = bbox.height - total_height - yb + font_descent 79 elif self.valign == self.CENTER: 80 # Centered vertical alignment 81 dy = (bbox.height - total_height - yb + font_descent + line_height) / 2. 82 else: 83 # Top vertical alignment 84 dy = line_height 85 86 for ref_x, ref_y, line in text_layout: 87 ctx.move_to(ref_x, ref_y + dy) 88 ctx.show_text(line) 89 ctx.new_path() 90
91 - def get_text_layout(self, x = None, y = None, width = None, wrap = False):
92 """Calculates the layout of the current text. `x` and `y` denote the 93 coordinates where the drawing should start. If they are both ``None``, 94 the current position of the context will be used. 95 96 Vertical alignment settings are not taken into account in this method 97 as the text is not drawn within a box. 98 99 :Parameters: 100 x : float or ``None`` 101 The X coordinate of the reference point where the layout should 102 start. 103 y : float or ``None`` 104 The Y coordinate of the reference point where the layout should 105 start. 106 width : float or ``None`` 107 The width of the box in which the text will be fitted. It matters 108 only when the text is right-aligned or centered. The text will 109 overflow the box if any of the lines is longer than the box width 110 and `wrap` is ``False``. 111 wrap : boolean 112 whether to allow re-wrapping of the text if it does not fit 113 within the given width. 114 115 :Returns: 116 a list consisting of ``(x, y, line)`` tuples where ``x`` and ``y`` 117 refer to reference points on the Cairo canvas and ``line`` refers 118 to the corresponding text that should be plotted there. 119 """ 120 ctx = self.context 121 122 if x is None or y is None: 123 x, y = ctx.get_current_point() 124 125 line_height = ctx.font_extents()[2] 126 127 if wrap: 128 if width and width > 0: 129 iterlines = self._iterlines_wrapped(width) 130 else: 131 warn("ignoring wrap=True as no width was specified") 132 else: 133 iterlines = self._iterlines() 134 135 result = [] 136 137 if self.halign == self.CENTER: 138 # Centered alignment 139 if width is None: 140 width = self.text_extents()[2] 141 for line, line_width, x_bearing in iterlines: 142 result.append((x + (width-line_width)/2. - x_bearing, y, line)) 143 y += line_height 144 145 elif self.halign == self.RIGHT: 146 # Right alignment 147 if width is None: 148 width = self.text_extents()[2] 149 x += width 150 for line, line_width, x_bearing in iterlines: 151 result.append((x - line_width - x_bearing, y, line)) 152 y += line_height 153 154 else: 155 # Left alignment 156 for line, _, x_bearing in iterlines: 157 result.append((x-x_bearing, y, line)) 158 y += line_height 159 160 return result 161
162 - def draw_at(self, x = None, y = None, width = None, wrap = False):
163 """Draws the text by setting up an appropriate path on the Cairo 164 context and filling it. `x` and `y` denote the coordinates where the 165 drawing should start. If they are both ``None``, the current position 166 of the context will be used. 167 168 Vertical alignment settings are not taken into account in this method 169 as the text is not drawn within a box. 170 171 :Parameters: 172 x : float or ``None`` 173 The X coordinate of the reference point where the drawing should 174 start. 175 y : float or ``None`` 176 The Y coordinate of the reference point where the drawing should 177 start. 178 width : float or ``None`` 179 The width of the box in which the text will be fitted. It matters 180 only when the text is right-aligned or centered. The text will 181 overflow the box if any of the lines is longer than the box width. 182 wrap : boolean 183 whether to allow re-wrapping of the text if it does not fit 184 within the given width. 185 """ 186 ctx = self.context 187 for ref_x, ref_y, line in self.get_text_layout(x, y, width, wrap): 188 ctx.move_to(ref_x, ref_y) 189 ctx.show_text(line) 190 ctx.new_path() 191
192 - def _iterlines(self):
193 """Iterates over the label line by line and returns a tuple containing 194 the folloing for each line: the line itself, the width of the line and 195 the X-bearing of the line.""" 196 ctx = self.context 197 for line in self._text.split("\n"): 198 xb, _, line_width, _, _, _ = ctx.text_extents(line) 199 yield (line, line_width, xb) 200
201 - def _iterlines_wrapped(self, width):
202 """Iterates over the label line by line and returns a tuple containing 203 the folloing for each line: the line itself, the width of the line and 204 the X-bearing of the line. 205 206 The difference between this method and `_iterlines()` is that this 207 method is allowed to re-wrap the line if necessary. 208 209 :Parameters: 210 width : float or ``None`` 211 The width of the box in which the text will be fitted. Lines will 212 be wrapped if they are wider than this width. 213 """ 214 ctx = self.context 215 for line in self._text.split("\n"): 216 xb, _, line_width, _, _, _ = ctx.text_extents(line) 217 if line_width <= width: 218 yield (line, line_width, xb) 219 continue 220 221 # We have to wrap the line 222 current_line, current_width, last_sep_width = [], 0, 0 223 for match in re.finditer(r"(\S+)(\s+)?", line): 224 word, sep = match.groups() 225 word_width = ctx.text_extents(word)[4] 226 if sep: 227 sep_width = ctx.text_extents(sep)[4] 228 else: 229 sep_width = 0 230 current_width += word_width 231 if current_width >= width and current_line: 232 yield ("".join(current_line), current_width - word_width, 0) 233 # Starting a new line 234 current_line, current_width = [word], word_width 235 if sep is not None: 236 current_line.append(sep) 237 else: 238 current_width += last_sep_width 239 current_line.append(word) 240 if sep is not None: 241 current_line.append(sep) 242 last_sep_width = sep_width 243 if current_line: 244 yield ("".join(current_line), current_width, 0) 245 246 @property
247 - def text(self):
248 """Returns the text to be drawn.""" 249 return self._text 250 251 @text.setter
252 - def text(self, text):
253 """Sets the text that will be drawn. 254 255 If `text` is ``None``, it will be mapped to an empty string; otherwise, 256 it will be converted to a string.""" 257 if text is None: 258 self._text = "" 259 else: 260 self._text = str(text) 261
262 - def text_extents(self):
263 """Returns the X-bearing, Y-bearing, width, height, X-advance and 264 Y-advance of the text. 265 266 For multi-line text, the X-bearing and Y-bearing correspond to the 267 first line, while the X-advance is extracted from the last line. 268 and the Y-advance is the sum of all the Y-advances. The width and 269 height correspond to the entire bounding box of the text.""" 270 lines = self.text.split("\n") 271 if len(lines) <= 1: 272 return self.context.text_extents(self.text) 273 274 x_bearing, y_bearing, width, height, x_advance, y_advance = \ 275 self.context.text_extents(lines[0]) 276 277 line_height = self.context.font_extents()[2] 278 for line in lines[1:]: 279 _, _, w, _, x_advance, ya = self.context.text_extents(line) 280 width = max(width, w) 281 height += line_height 282 y_advance += ya 283 284 return x_bearing, y_bearing, width, height, x_advance, y_advance
285
286 -def test():
287 """Testing routine for L{TextDrawer}""" 288 import math 289 from igraph.drawing.utils import find_cairo 290 cairo = find_cairo() 291 292 text = "The quick brown fox\njumps over a\nlazy dog" 293 width, height = (600, 1000) 294 295 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) 296 context = cairo.Context(surface) 297 drawer = TextDrawer(context, text) 298 299 context.set_source_rgb(1, 1, 1) 300 context.set_font_size(16.) 301 context.rectangle(0, 0, width, height) 302 context.fill() 303 304 context.set_source_rgb(0.5, 0.5, 0.5) 305 for i in range(200, width, 200): 306 context.move_to(i, 0) 307 context.line_to(i, height) 308 context.stroke() 309 for i in range(200, height, 200): 310 context.move_to(0, i) 311 context.line_to(width, i) 312 context.stroke() 313 context.set_source_rgb(0.75, 0.75, 0.75) 314 context.set_line_width(0.5) 315 for i in range(100, width, 200): 316 context.move_to(i, 0) 317 context.line_to(i, height) 318 context.stroke() 319 for i in range(100, height, 200): 320 context.move_to(0, i) 321 context.line_to(width, i) 322 context.stroke() 323 324 def mark_point(red, green, blue): 325 """Marks the current point on the canvas by the given color""" 326 x, y = context.get_current_point() 327 context.set_source_rgba(red, green, blue, 0.5) 328 context.arc(x, y, 4, 0, 2 * math.pi) 329 context.fill() 330 331 # Testing drawer.draw_at() 332 for i, halign in enumerate(("left", "center", "right")): 333 # Mark the reference points 334 context.move_to(i * 200, 40) 335 mark_point(0, 0, 1) 336 context.move_to(i * 200, 140) 337 mark_point(0, 0, 1) 338 339 # Draw the text 340 context.set_source_rgb(0, 0, 0) 341 drawer.halign = halign 342 drawer.draw_at(i * 200, 40) 343 drawer.draw_at(i * 200, 140, width=200) 344 345 # Mark the new reference point 346 mark_point(1, 0, 0) 347 348 # Testing TextDrawer.draw() 349 for i, halign in enumerate(("left", "center", "right")): 350 for j, valign in enumerate(("top", "center", "bottom")): 351 # Draw the text 352 context.set_source_rgb(0, 0, 0) 353 drawer.halign = halign 354 drawer.valign = valign 355 drawer.bbox = (i*200, j*200+200, i*200+200, j*200+400) 356 drawer.draw() 357 # Mark the new reference point 358 mark_point(1, 0, 0) 359 360 # Testing TextDrawer.wrap() 361 drawer.text = "Jackdaws love my big sphinx of quartz. Yay, wrapping! " + \ 362 "Jackdaws love my big sphinx of quartz.\n\n" + \ 363 "Jackdaws love my big sphinx of quartz." 364 drawer.valign = TextDrawer.TOP 365 for i, halign in enumerate(("left", "center", "right")): 366 context.move_to(i * 200, 840) 367 368 # Mark the reference point 369 mark_point(0, 0, 1) 370 371 # Draw the text 372 context.set_source_rgb(0, 0, 0) 373 drawer.halign = halign 374 drawer.draw_at(i * 200, 840, width=199, wrap=True) 375 376 # Mark the new reference point 377 mark_point(1, 0, 0) 378 379 surface.write_to_png("test.png") 380 381 if __name__ == "__main__": 382 test() 383

   Home       Trees       Indices       Help