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.

Line 
1"""
2===================
3Render (``render``)
4===================
5
6.. index:: utils
7.. index::
8   single: utils; render
9
10"""
11
12from __future__ import with_statement
13
14import os
15import sys
16import math
17import warnings
18import StringIO
19import gc
20import copy
21import binascii
22
23from functools import wraps
24from contextlib import contextmanager
25
26import numpy
27
28from Orange.utils.environ import install_dir
29
30
31def _pil_safe_import(module):
32
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
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
109
110def with_gc_disabled(func):
111    def disabler():
112        gc.disable()
113        try:
114            yield
115        finally:
116            gc.enable()
117
118    @wraps(func)
119    def wrapper(*args, **kwargs):
120        with contextmanager(disabler)():
121            return func(*args, **kwargs)
122    return wrapper
123
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
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
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:
144            return tuple(self.colors[-1][i] for i in range(3))  # self.colors[-1].green(), self.colors[-1].blue())
145        else:
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()
148            x = val * (len(self.colors) - 1) - index
149            if gamma is not None:
150                x = self.gamma_func(x, gamma)
151            return [(c2 - c1) * x + c1 for c1, c2 in [(red1, red2), (green1, green2), (blue1, blue2)]]
152
153    def __call__(self, val, gamma=None):
154        return self.get_rgb(val, gamma)
155
156
157def as_open_file(file, mode="rb"):
158    if isinstance(file, basestring):
159        file = open(file, mode)
160    else:  # assuming it is file like with proper mode, could check for write, read
161        pass
162    return file
163
164
165class Renderer(object):
166    render_state_attributes = ["font", "stroke_color", "fill_color", "render_hints", "transform", "gradient", "text_alignment"]
167
168    ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER = range(3)
169
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 = []
184
185    def font(self):
186        return self.render_state["font"]
187
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"]
193
194    def set_fill_color(self, color):
195        self.render_state["fill_color"] = color
196
197    def set_gradient(self, gradient):
198        self.render_state["gradient"] = gradient
199
200    def gradient(self):
201        return self.render_state["gradient"]
202
203    def stroke_color(self):
204        return self.render_state["stroke_color"]
205
206    def set_stroke_color(self, color):
207        self.render_state["stroke_color"] = color
208
209    def stroke_width(self):
210        return self.render_state["stroke_width"]
211
212    def set_stroke_width(self, width):
213        self.render_state["stroke_width"] = width
214
215    def set_text_alignment(self, align):
216        self.render_state["text_alignment"] = align
217
218    def text_alignment(self):
219        return self.render_state["text_alignment"]
220
221    def transform(self):
222        return self.render_state["transform"]
223
224    def set_transform(self, transform):
225        self.render_state["transform"] = transform
226
227    def render_hints(self):
228        return self.render_state["render_hints"]
229
230    def set_render_hints(self, hints):
231        self.render_state["render_hints"].update(hints)
232
233    def save_render_state(self):
234        self.render_state_stack.append(copy.deepcopy(self.render_state))
235
236    def restore_render_state(self):
237        self.render_state = self.render_state_stack.pop(-1)
238
239    def apply_transform(self, transform):
240        self.render_state["transform"] = self.render_state["transform"] * transform
241
242    def translate(self, x, y):
243        transform = numpy.eye(3)
244        transform[:, 2] = x, y, 1
245        self.apply_transform(transform)
246
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)
252
253    def scale(self, sx, sy):
254        transform = numpy.eye(3)
255        transform[(0, 1), (0, 1)] = sx, sy
256        self.apply_transform(transform)
257
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)
262
263    def draw_line(self, sx, sy, ex, ey, **kwargs):
264        raise NotImplementedError
265
266    def draw_lines(self, points, **kwargs):
267        raise NotImplementedError
268
269    def draw_rect(self, x, y, w, h, **kwargs):
270        raise NotImplementedError
271
272    def draw_polygon(self, vertices, **kwargs):
273        raise NotImplementedError
274
275    def draw_arch(self, something, **kwargs):
276        raise NotImplementedError
277
278    def draw_text(self, x, y, text, **kwargs):
279        raise NotImplementedError
280
281    def string_size_hint(self, text, **kwargs):
282        raise NotImplemented
283
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()
296
297    def save(self, file):
298        raise NotImplementedError
299
300    def close(self, file):
301        pass
302
303
304class EPSRenderer(Renderer):
305    EPS_DRAW_RECT = """/draw_rect
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"""
315
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
345
346/right_align_show
347{ dup stringwidth pop
348  neg
349  0 rmoveto
350  show } def
351"""
352
353    def __init__(self, width, height):
354        Renderer.__init__(self, width, height)
355        self._eps = StringIO.StringIO()
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),
361                                 fill_color=lambda color: "%f %f %f setrgbcolor" % tuple(255.0 / c for c in color),
362                                 stroke_width=lambda w: "%f setlinewidth" % w)
363
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())
367
368    def set_fill_color(self, color):
369        Renderer.set_fill_color(self, color)
370        self._eps.write("%f %f %f setrgbcolor\n" % tuple(c / 255.0 for c in color))
371
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])
376        self._eps.write(self.EPS_SET_GRADIENT % (x1, y1, x2, y2, len(samples), binascii.hexlify(binary)))
377
378    def set_stroke_color(self, color):
379        Renderer.set_stroke_color(self, color)
380        self._eps.write("%f %f %f setrgbcolor\n" % tuple(c / 255.0 for c in color))
381
382    def set_stroke_width(self, width):
383        Renderer.set_stroke_width(self, width)
384        self._eps.write("%f setlinewidth\n" % width)
385
386    def set_render_hints(self, hints):
387        Renderer.set_render_hints(self, hints)
388        if hints.get("linecap", None):
389            map = {"butt": 0, "round": 1, "rect": 2}
390            self._eps.write("%i setlinecap\n" % (map.get(hints.get("linecap"), 0)))
391
392    @with_state
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))
395
396    @with_state
397    def draw_rect(self, x, y, w, h, **kwargs):
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))
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")
407
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")
419
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))
424
425    def save_render_state(self):
426        Renderer.save_render_state(self)
427        self._eps.write("gsave\n")
428
429    def restore_render_state(self):
430        Renderer.restore_render_state(self)
431        self._eps.write("grestore\n")
432
433    def translate(self, dx, dy):
434        Renderer.translate(self, dx, dy)
435        self._eps.write("%f %f translate\n" % (dx, -dy))
436
437    def rotate(self, angle):
438        Renderer.rotate(self, angle)
439        self._eps.write("%f rotate\n" % -angle)
440
441    def scale(self, sx, sy):
442        Renderer.scale(self, sx, sy)
443        self._eps.write("%f %f scale\n" % (sx, sy))
444
445    def skew(self, sx, sy):
446        Renderer.skew(self, sx, sy)
447        self._eps.write("%f %f skew\n" % (sx, sy))
448
449    def save(self, file):
450        file = as_open_file(file, "wb")
451        file.write(self._eps.getvalue())
452
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]
456
457
458def _int_color(color):
459    """ Transform the color tuple (with floats) to tuple with ints
460    (needed by PIL)
461    """
462    return tuple(map(int, color))
463
464
465class PILRenderer(Renderer):
466    def __init__(self, width, height):
467        Renderer.__init__(self, width, height)
468        from PIL import Image, ImageDraw, ImageFont
469        self._pil_image = Image.new("RGB", (int(width), int(height)), (255, 255, 255))
470        self._draw = ImageDraw.Draw(self._pil_image, "RGB")
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]
476
477    def set_font(self, family, size):
478        Renderer.set_font(self, family, size)
479        from PIL import ImageFont
480        try:
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))
486        except Exception:
487            warnings.warn("Could not load %s.ttf font!" % family, stacklevel=2)
488            try:
489                self._pil_font = ImageFont.truetype("cour.ttf", int(size))
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()
493
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)
498        self._draw.line((sx, sy, ex, ey), fill=_int_color(self.stroke_color()),
499                        width=int(self.stroke_width()))
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)
505        self._draw.rectangle((x1, y1, x2, y2), fill=_int_color(self.fill_color()),
506                             outline=_int_color(self.stroke_color()))
507
508    @with_state
509    def draw_text(self, x, y, text, **kwargs):
510        x, y = self._transform(x, y - self.font()[1])
511        self._draw.text((x, y), text, font=self._pil_font,
512                        fill=_int_color(self.stroke_color()))
513
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)
520
521    def string_size_hint(self, text, **kwargs):
522        return self._pil_font.getsize(text)[1]
523
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"""
534
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 = {}
541
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)
549
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')
554
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()
560
561    def get_stroke(self):
562        return 'stroke="rgb(%i, %i, %i)"' % self.stroke_color() + ' stroke-width="%f"' % self.stroke_width()
563
564    def get_text_alignment(self):
565        return 'text-anchor="%s"' % (["start", "end", "middle"][self.text_alignment()])
566
567    def get_linecap(self):
568        return 'stroke-linecap="%s"' % self.render_hints().get("linecap", "butt")
569
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())))
573
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(),)))
577
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(),)))
584
585    @with_state
586    def draw_text(self, x, y, text):
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
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
592
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
596
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
600
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)
609
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)
614
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()))
618
619
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.