source: orange/Orange/utils/render.py @ 11457:a89187176fc8

Revision 11457:a89187176fc8, 20.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Added a workaround/fix for different PIL import conventions.

RevLine 
[8042]1"""
2===================
3Render (``render``)
4===================
5
[10582]6.. index:: utils
[8042]7.. index::
[10582]8   single: utils; render
[11457]9
[8042]10"""
11
12from __future__ import with_statement
13
[11457]14import os
[8053]15import sys
[11457]16import math
17import warnings
18import StringIO
19import gc
20import copy
21import binascii
22
23from functools import wraps
24from contextlib import contextmanager
25
[8042]26import numpy
[8053]27
[10633]28from Orange.utils.environ import install_dir
[8053]29
[8042]30
[11457]31def _pil_safe_import(module):
[8042]32
[11457]33    pil, module = module.split(".", 1)
34    if pil != "PIL":
35        raise ValueError("Expected a module name inside PIL namespace.")
36
37    if module in sys.modules:
38        # 'module' is already imported in the global namespace
39        pil_mod = sys.modules.get("PIL." + module)
40        if pil_mod is None:
41            # The PIL package was not yet imported.
42            try:
43                # Make sure "PIL" is in sys.modules
44                import PIL
45            except ImportError:
46                # PIL was installed as an egg (it's broken,
47                # PIL package can not be imported)
48                return None
49            # Insert 'module' into the PIL namespace
50            sys.modules["PIL." + module] = sys.modules[module]
51
52        return sys.modules["PIL." + module]
53
54    try:
55        mod = __import__("PIL." + module, fromlist=[module])
56    except ImportError:
57        mod = None
58
59    if mod is not None:
60        sys.modules[module] = mod
61
62    return mod
63
64# scipy and matplotlib both import modules from PIL but depending
65# on the installed version might use different conventions, i.e.
66#
67# >>> import Image
68# >>> from PIL import Image
69#
70# This can crash the python interpreter with a:
71# AccessInit: hash collision: 3 for both 1 and 1
72#
73# (This is a known common problem with PIL imports and is triggered
74# in libImaging/Access.c:add_item)
75#
76# So we preempt the imports and use a hack where we manually insert
77# modules from PIL namespace into global namespace or vice versa
78#
79# PIL is imported by (as currently imported from Orange):
80#    - 'scipy.utils.pilutils' (PIL.Image and PIL.ImageFilter)
81#    - 'matplotlib.backend_bases' (PIL.Image)
82#    - 'matplotlib.image' (PIL.Image)
83#
84# Based on grep 'import _imaging PIL/*.py the following modules
85# import C extensions:
86#    - Image (_imaging)
87#    - ImageCms (_imagingcms)
88#    - ImageDraw (_imagingagg)
89#    - ImageFont (_imagingft)
90#    - ImageGL (_imaginggl)
91#    - ImageMath (_imagingmath)
92#    - ImageTk (_imagingtk)
93#    - GifImagePlugin (_imaging_gif)
94#
95# However only '_imaging' seems to cause the crash.
96
97
98_pil_safe_import("PIL._imaging")
99
100
[8042]101def with_state(func):
102    @wraps(func)
103    def wrap(self, *args, **kwargs):
104        with self.state(**kwargs):
105            r = func(self, *args)
106        return r
107    return wrap
108
[11457]109
[8042]110def with_gc_disabled(func):
111    def disabler():
112        gc.disable()
113        try:
114            yield
115        finally:
116            gc.enable()
[11457]117
[8042]118    @wraps(func)
119    def wrapper(*args, **kwargs):
120        with contextmanager(disabler)():
121            return func(*args, **kwargs)
122    return wrapper
123
[8053]124
125class ColorPalette(object):
126    def __init__(self, colors, gamma=None, overflow=(255, 255, 255), underflow=(255, 255, 255), unknown=(0, 0, 0)):
127        self.colors = colors
[11457]128        self.gamma_func = lambda x, gamma: ((math.exp(gamma * math.log(2 * x - 1)) if x > 0.5 else -math.exp(gamma * math.log(-2 * x + 1)) if x != 0.5 else 0.0) + 1) / 2.0
[8053]129        self.gamma = gamma
130        self.overflow = overflow
131        self.underflow = underflow
132        self.unknown = unknown
133
134    def get_rgb(self, val, gamma=None):
135        if val is None:
136            return self.unknown
137        gamma = self.gamma if gamma is None else gamma
138        index = int(val * (len(self.colors) - 1))
139        if val < 0.0:
140            return self.underflow
141        elif val > 1.0:
142            return self.overflow
143        elif index == len(self.colors) - 1:
[11457]144            return tuple(self.colors[-1][i] for i in range(3))  # self.colors[-1].green(), self.colors[-1].blue())
[8053]145        else:
[11457]146            red1, green1, blue1 = [self.colors[index][i] for i in range(3)]  # , self.colors[index].green(), self.colors[index].blue()
147            red2, green2, blue2 = [self.colors[index + 1][i] for i in range(3)]  # , self.colors[index + 1].green(), self.colors[index + 1].blue()
[8053]148            x = val * (len(self.colors) - 1) - index
149            if gamma is not None:
[10076]150                x = self.gamma_func(x, gamma)
[8053]151            return [(c2 - c1) * x + c1 for c1, c2 in [(red1, red2), (green1, green2), (blue1, blue2)]]
[10630]152
[8053]153    def __call__(self, val, gamma=None):
154        return self.get_rgb(val, gamma)
[10630]155
[11457]156
[9033]157def as_open_file(file, mode="rb"):
158    if isinstance(file, basestring):
159        file = open(file, mode)
[11457]160    else:  # assuming it is file like with proper mode, could check for write, read
[9033]161        pass
162    return file
[8053]163
[11457]164
[8042]165class Renderer(object):
166    render_state_attributes = ["font", "stroke_color", "fill_color", "render_hints", "transform", "gradient", "text_alignment"]
[10630]167
[8042]168    ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER = range(3)
[10630]169
[8042]170    def __init__(self, width, height):
171        self.width = width
172        self.height = height
173        self.render_state = {}
174        self.render_state["font"] = ("Times-Roman", 10)
175        self.render_state["fill_color"] = (0, 0, 0)
176        self.render_state["gradient"] = {}
177        self.render_state["stroke_color"] = (0, 0, 0)
178        self.render_state["stroke_width"] = 1
179        self.render_state["text_alignment"] = self.ALIGN_LEFT
180        self.render_state["transform"] = numpy.matrix(numpy.eye(3))
181        self.render_state["view_transform"] = numpy.matrix(numpy.eye(3))
182        self.render_state["render_hints"] = {}
183        self.render_state_stack = []
[10630]184
[8042]185    def font(self):
186        return self.render_state["font"]
[10630]187
[8042]188    def set_font(self, family, size):
189        self.render_state["font"] = family, size
190
191    def fill_color(self):
192        return self.render_state["fill_color"]
[10630]193
[8042]194    def set_fill_color(self, color):
195        self.render_state["fill_color"] = color
[10630]196
[8042]197    def set_gradient(self, gradient):
198        self.render_state["gradient"] = gradient
[10630]199
[8042]200    def gradient(self):
201        return self.render_state["gradient"]
[10630]202
[8042]203    def stroke_color(self):
204        return self.render_state["stroke_color"]
[10630]205
[8042]206    def set_stroke_color(self, color):
207        self.render_state["stroke_color"] = color
[10630]208
[8042]209    def stroke_width(self):
210        return self.render_state["stroke_width"]
[10630]211
[8042]212    def set_stroke_width(self, width):
213        self.render_state["stroke_width"] = width
[10630]214
[8042]215    def set_text_alignment(self, align):
216        self.render_state["text_alignment"] = align
[10630]217
[8042]218    def text_alignment(self):
219        return self.render_state["text_alignment"]
[10630]220
[8042]221    def transform(self):
222        return self.render_state["transform"]
[10630]223
[8042]224    def set_transform(self, transform):
225        self.render_state["transform"] = transform
[10630]226
[8042]227    def render_hints(self):
228        return self.render_state["render_hints"]
[10630]229
[8042]230    def set_render_hints(self, hints):
231        self.render_state["render_hints"].update(hints)
[10630]232
[8042]233    def save_render_state(self):
234        self.render_state_stack.append(copy.deepcopy(self.render_state))
[10630]235
[8042]236    def restore_render_state(self):
237        self.render_state = self.render_state_stack.pop(-1)
[10630]238
[8042]239    def apply_transform(self, transform):
240        self.render_state["transform"] = self.render_state["transform"] * transform
[10630]241
[8042]242    def translate(self, x, y):
243        transform = numpy.eye(3)
244        transform[:, 2] = x, y, 1
245        self.apply_transform(transform)
[10630]246
[8042]247    def rotate(self, angle):
248        angle *= 2 * math.pi / 360.0
249        transform = numpy.eye(3)
250        transform[:2, :2] = [[math.cos(angle), -math.sin(angle)], [math.sin(angle), math.cos(angle)]]
251        self.apply_transform(transform)
[10630]252
[8042]253    def scale(self, sx, sy):
254        transform = numpy.eye(3)
[10630]255        transform[(0, 1), (0, 1)] = sx, sy
[8042]256        self.apply_transform(transform)
[10630]257
[8042]258    def skew(self, sx, sy):
259        transform = numpy.eye(3)
260        transform[(1, 0), (0, 1)] = numpy.array([sx, sy]) * 2 * math.pi / 360.0
261        self.apply_transform(transform)
[10630]262
[8042]263    def draw_line(self, sx, sy, ex, ey, **kwargs):
[9033]264        raise NotImplementedError
[10630]265
[8042]266    def draw_lines(self, points, **kwargs):
[9033]267        raise NotImplementedError
[10630]268
[8042]269    def draw_rect(self, x, y, w, h, **kwargs):
[9033]270        raise NotImplementedError
[10630]271
[8042]272    def draw_polygon(self, vertices, **kwargs):
[9033]273        raise NotImplementedError
[8042]274
275    def draw_arch(self, something, **kwargs):
[9033]276        raise NotImplementedError
[10630]277
[8042]278    def draw_text(self, x, y, text, **kwargs):
[9033]279        raise NotImplementedError
[10630]280
[8042]281    def string_size_hint(self, text, **kwargs):
[11457]282        raise NotImplemented
[10630]283
[8042]284    @contextmanager
285    def state(self, **kwargs):
286        self.save_render_state()
287        for key, value in kwargs.items():
288            if key in ["translate", "rotate", "scale", "skew"]:
289                getattr(self, key)(*value)
290            else:
291                getattr(self, "set_" + key)(value)
292        try:
293            yield
294        finally:
295            self.restore_render_state()
[10630]296
[9033]297    def save(self, file):
298        raise NotImplementedError
[10630]299
[8042]300    def close(self, file):
301        pass
[10630]302
[11457]303
[8042]304class EPSRenderer(Renderer):
[11457]305    EPS_DRAW_RECT = """/draw_rect
[8042]306{/h exch def /w exch def
307 /y exch def /x exch def
308 newpath
309 x y moveto
310 w 0 rlineto
311 0 h neg rlineto
312 w neg 0 rlineto
313 closepath
314} def"""
[10630]315
[8042]316    EPS_SET_GRADIENT = """<< /PatternType 2
317 /Shading
318   << /ShadingType 2
319      /ColorSpace /DeviceRGB
320      /Coords [%f %f %f %f]
321      /Function
322      << /FunctionType 0
323         /Domain [0 1]
324         /Range [0 1 0 1 0 1]
325         /BitsPerSample 8
326         /Size [%i]
327         /DataSource <%s>
328      >>
329   >>
330>>
331matrix
332makepattern
333/mypattern exch def
334/Pattern setcolorspace
335mypattern setcolor
336
337"""
338
339    EPS_SHOW_FUNCTIONS = """/center_align_show
340{ dup stringwidth pop
341  2 div
342  neg
343  0 rmoveto
344  show } def
[11457]345
[8042]346/right_align_show
347{ dup stringwidth pop
348  neg
349  0 rmoveto
350  show } def
351"""
[11457]352
[8042]353    def __init__(self, width, height):
354        Renderer.__init__(self, width, height)
[11457]355        self._eps = StringIO.StringIO()
[8042]356        self._eps.write("%%!PS-Adobe-3.0 EPSF-3.0\n%%%%BoundingBox: 0 0 %i %i\n" % (width, height))
357        self._eps.write(self.EPS_SHOW_FUNCTIONS)
358        self._eps.write("%f %f translate\n" % (0, self.height))
359        self.set_font(*self.render_state["font"])
360        self._inline_func = dict(stroke_color=lambda color: "%f %f %f setrgbcolor" % tuple(255.0 / c for c in color),
[11457]361                                 fill_color=lambda color: "%f %f %f setrgbcolor" % tuple(255.0 / c for c in color),
[8042]362                                 stroke_width=lambda w: "%f setlinewidth" % w)
[10630]363
[8042]364    def set_font(self, family, size):
365        Renderer.set_font(self, family, size)
366        self._eps.write("/%s findfont %f scalefont setfont\n" % self.font())
[10630]367
[8042]368    def set_fill_color(self, color):
369        Renderer.set_fill_color(self, color)
[10630]370        self._eps.write("%f %f %f setrgbcolor\n" % tuple(c / 255.0 for c in color))
371
[8042]372    def set_gradient(self, gradient):
373        Renderer.set_gradient(self, gradient)
374        (x1, y1, x2, y2), samples = gradient
375        binary = "".join([chr(int(c)) for p, s in samples for c in s])
[10630]376        self._eps.write(self.EPS_SET_GRADIENT % (x1, y1, x2, y2, len(samples), binascii.hexlify(binary)))
377
[8042]378    def set_stroke_color(self, color):
379        Renderer.set_stroke_color(self, color)
[10630]380        self._eps.write("%f %f %f setrgbcolor\n" % tuple(c / 255.0 for c in color))
381
[8042]382    def set_stroke_width(self, width):
383        Renderer.set_stroke_width(self, width)
384        self._eps.write("%f setlinewidth\n" % width)
[10630]385
[8042]386    def set_render_hints(self, hints):
387        Renderer.set_render_hints(self, hints)
388        if hints.get("linecap", None):
[11457]389            map = {"butt": 0, "round": 1, "rect": 2}
[8042]390            self._eps.write("%i setlinecap\n" % (map.get(hints.get("linecap"), 0)))
[10630]391
392    @with_state
[8042]393    def draw_line(self, sx, sy, ex, ey, **kwargs):
394        self._eps.write("newpath\n%f %f moveto %f %f lineto\nstroke\n" % (sx, -sy, ex, -ey))
[10630]395
[8042]396    @with_state
397    def draw_rect(self, x, y, w, h, **kwargs):
[11457]398        self._eps.write("newpath\n%(x)f %(y)f moveto %(w)f 0 rlineto\n0 %(h)f rlineto %(w)f neg 0 rlineto\nclosepath\n" % dict(x=x, y=-y, w=w, h=-h))
[8042]399        self._eps.write("gsave\n")
400        if self.gradient():
401            self.set_gradient(self.gradient())
402        else:
403            self.set_fill_color(self.fill_color())
404        self._eps.write("fill\ngrestore\n")
405        self.set_stroke_color(self.stroke_color())
406        self._eps.write("stroke\n")
[10630]407
[8042]408    @with_state
409    def draw_polygon(self, vertices, **kwargs):
410        self._eps.write("newpath\n%f %f moveto\n" % vertices[0])
411        for x, y in vertices[1:]:
412            self._eps.write("%f %f lineto\n" % (x, y))
413        self._eps.write("closepath\n")
414        self._eps.write("gsave\n")
415        self.set_fill_color(self.fill_color())
416        self._eps.write("fill\ngrestore\n")
417        self.set_stroke_color(self.stroke_color())
418        self._eps.write("stroke\n")
[10630]419
[8042]420    @with_state
421    def draw_text(self, x, y, text, **kwargs):
422        show = ["show", "right_align_show", "center_align_show"][self.text_alignment()]
423        self._eps.write("%f %f moveto (%s) %s\n" % (x, -y, text, show))
[10630]424
[8042]425    def save_render_state(self):
426        Renderer.save_render_state(self)
427        self._eps.write("gsave\n")
[10630]428
[8042]429    def restore_render_state(self):
430        Renderer.restore_render_state(self)
431        self._eps.write("grestore\n")
[10630]432
[8042]433    def translate(self, dx, dy):
434        Renderer.translate(self, dx, dy)
435        self._eps.write("%f %f translate\n" % (dx, -dy))
[10630]436
[8042]437    def rotate(self, angle):
438        Renderer.rotate(self, angle)
439        self._eps.write("%f rotate\n" % -angle)
[10630]440
[8042]441    def scale(self, sx, sy):
442        Renderer.scale(self, sx, sy)
443        self._eps.write("%f %f scale\n" % (sx, sy))
[10630]444
[8042]445    def skew(self, sx, sy):
446        Renderer.skew(self, sx, sy)
447        self._eps.write("%f %f skew\n" % (sx, sy))
[10630]448
[9033]449    def save(self, file):
450        file = as_open_file(file, "wb")
451        file.write(self._eps.getvalue())
[10630]452
[8042]453    def string_size_hint(self, text, **kwargs):
454        warnings.warn("EpsRenderer class does not suport exact string width estimation", stacklevel=2)
455        return len(text) * self.font()[1]
[10630]456
[11457]457
[8053]458def _int_color(color):
459    """ Transform the color tuple (with floats) to tuple with ints
[11457]460    (needed by PIL)
[8053]461    """
462    return tuple(map(int, color))
463
[11457]464
[8042]465class PILRenderer(Renderer):
466    def __init__(self, width, height):
467        Renderer.__init__(self, width, height)
[11457]468        from PIL import Image, ImageDraw, ImageFont
[8053]469        self._pil_image = Image.new("RGB", (int(width), int(height)), (255, 255, 255))
[10630]470        self._draw = ImageDraw.Draw(self._pil_image, "RGB")
[8042]471        self._pil_font = ImageFont.load_default()
472
473    def _transform(self, x, y):
474        p = self.transform() * [[x], [y], [1]]
475        return p[0, 0], p[1, 0]
[10630]476
[8042]477    def set_font(self, family, size):
478        Renderer.set_font(self, family, size)
[11457]479        from PIL import ImageFont
[8042]480        try:
[10630]481            font_file = os.path.join(install_dir, "utils", family + ".ttf")
482            if os.path.exists(font_file):
483                self._pil_font = ImageFont.truetype(font_file, int(size))
484            else:
485                self._pil_font = ImageFont.truetype(family + ".ttf", int(size))
[8042]486        except Exception:
[10630]487            warnings.warn("Could not load %s.ttf font!" % family, stacklevel=2)
[8042]488            try:
[10630]489                self._pil_font = ImageFont.truetype("cour.ttf", int(size))
[8042]490            except Exception:
491                warnings.warn("Could not load the cour.ttf font!! Loading the default", stacklevel=2)
492                self._pil_font = ImageFont.load_default()
[10630]493
[8042]494    @with_state
495    def draw_line(self, sx, sy, ex, ey, **kwargs):
496        sx, sy = self._transform(sx, sy)
497        ex, ey = self._transform(ex, ey)
[8053]498        self._draw.line((sx, sy, ex, ey), fill=_int_color(self.stroke_color()),
499                        width=int(self.stroke_width()))
[8042]500
501    @with_state
502    def draw_rect(self, x, y, w, h, **kwargs):
503        x1, y1 = self._transform(x, y)
504        x2, y2 = self._transform(x + w, y + h)
[11457]505        self._draw.rectangle((x1, y1, x2, y2), fill=_int_color(self.fill_color()),
[8053]506                             outline=_int_color(self.stroke_color()))
[10630]507
[8042]508    @with_state
509    def draw_text(self, x, y, text, **kwargs):
510        x, y = self._transform(x, y - self.font()[1])
[8053]511        self._draw.text((x, y), text, font=self._pil_font,
512                        fill=_int_color(self.stroke_color()))
[10630]513
[9033]514    def save(self, file, format=None):
515        if isinstance(file, basestring):
516            self._pil_image.save(file)
517        else:
518            file = as_open_file(file, "wb")
519            self._pil_image.save(file, format)
[10630]520
[8042]521    def string_size_hint(self, text, **kwargs):
522        return self._pil_font.getsize(text)[1]
[10630]523
[8042]524
525class SVGRenderer(Renderer):
526    SVG_HEADER = """<?xml version="1.0" ?>
527<svg height="%f" version="1.0" width="%f" xmlns="http://www.w3.org/2000/svg">
528<defs>
529    %s
530</defs>
531    %s
532</svg>
533"""
[11457]534
[8042]535    def __init__(self, width, height):
536        Renderer.__init__(self, width, height)
537        self.transform_count_stack = [0]
538        self._svg = StringIO.StringIO()
539        self._defs = StringIO.StringIO()
540        self._gradients = {}
[10630]541
[8042]542    def set_gradient(self, gradient):
543        Renderer.set_gradient(self, gradient)
544        if gradient not in self._gradients.items():
545            id = "grad%i" % len(self._gradients)
546            self._gradients[id] = gradient
547            (x1, y1, x2, y2), stops = gradient
548            (x1, y1, x2, y2) = (0, 0, 100, 0)
[10630]549
[8042]550            self._defs.write('<linearGradient id="%s" x1="%f%%" y1="%f%%" x2="%f%%" y2="%f%%">\n' % (id, x1, y1, x2, y2))
551            for offset, color in stops:
552                self._defs.write('<stop offset="%f" style="stop-color:rgb(%i, %i, %i); stop-opacity:1"/>\n' % ((offset,) + color))
553            self._defs.write('</linearGradient>\n')
[10630]554
[8042]555    def get_fill(self):
556        if self.render_state["gradient"]:
557            return 'style="fill:url(#%s)"' % ([key for key, gr in self._gradients.items() if gr == self.render_state["gradient"]][0])
558        else:
559            return 'fill="rgb(%i %i %i)"' % self.fill_color()
[10630]560
[8042]561    def get_stroke(self):
[11457]562        return 'stroke="rgb(%i, %i, %i)"' % self.stroke_color() + ' stroke-width="%f"' % self.stroke_width()
[10630]563
[8042]564    def get_text_alignment(self):
565        return 'text-anchor="%s"' % (["start", "end", "middle"][self.text_alignment()])
[10630]566
[8042]567    def get_linecap(self):
568        return 'stroke-linecap="%s"' % self.render_hints().get("linecap", "butt")
[10630]569
[8042]570    @with_state
571    def draw_line(self, sx, sy, ex, ey):
572        self._svg.write('<line x1="%f" y1="%f" x2="%f" y2="%f" %s %s/>\n' % ((sx, sy, ex, ey) + (self.get_stroke(), self.get_linecap())))
[10630]573
[8042]574    @with_state
575    def draw_rect(self, x, y, w, h):
576        self._svg.write('<rect x="%f" y="%f" width="%f" height="%f" %s %s/>\n' % ((x, y, w, h) + (self.get_fill(),) + (self.get_stroke(),)))
[10630]577
[8042]578    @with_state
579    def draw_polygon(self, vertices, **kwargs):
580        path = "M %f %f L " % vertices[0]
581        path += " L ".join("%f %f" % vert for vert in vertices[1:])
582        path += " z"
583        self._svg.write('<path d="%s" %s/>' % ((path,) + (self.get_stroke(),)))
[10630]584
[8042]585    @with_state
586    def draw_text(self, x, y, text):
[10630]587        self._svg.write('<text x="%f" y="%f" font-family="%s" font-size="%f" %s>%s</text>\n' % ((x, y) + self.font() + (self.get_text_alignment(), text)))
588
[8042]589    def translate(self, x, y):
590        self._svg.write('<g transform="translate(%f,%f)">\n' % (x, y))
591        self.transform_count_stack[-1] = self.transform_count_stack[-1] + 1
[10630]592
[8042]593    def rotate(self, angle):
594        self._svg.write('<g transform="rotate(%f)">\n' % angle)
595        self.transform_count_stack[-1] = self.transform_count_stack[-1] + 1
[10630]596
[8042]597    def scale(self, sx, sy):
598        self._svg.write('<g transform="scale(%f,%f)">\n' % (sx, sy))
599        self.transform_count_stack[-1] = self.transform_count_stack[-1] + 1
[10630]600
[8042]601    def skew(self, sx, sy):
602        self._svg.write('<g transform="skewX(%f)">' % sx)
603        self._svg.write('<g transform="skewY(%f)">' % sy)
604        self.transform_count_stack[-1] = self.transform_count_stack[-1] + 2
605
606    def save_render_state(self):
607        Renderer.save_render_state(self)
608        self.transform_count_stack.append(0)
[10630]609
[8042]610    def restore_render_state(self):
611        Renderer.restore_render_state(self)
612        count = self.transform_count_stack.pop(-1)
613        self._svg.write('</g>\n' * count)
[10630]614
[9033]615    def save(self, file):
616        file = as_open_file(file, "wb")
617        file.write(self.SVG_HEADER % (self.height, self.width, self._defs.getvalue(), self._svg.getvalue()))
[10630]618
[11457]619
[8042]620class CairoRenderer(Renderer):
621    def __init__(self, width, height):
622        Renderer.__init__(self, width, height)
Note: See TracBrowser for help on using the repository browser.