source: orange/Orange/utils/render.py @ 10633:fb05a6f3a235

Revision 10633:fb05a6f3a235, 19.6 KB checked in by mstajdohar, 2 years ago (diff)

Changed obsolete names.

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