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