source: orange/orange/misc/render.py @ 9669:165371b04b4a

Revision 9669:165371b04b4a, 19.9 KB checked in by anze <anze.staric@…>, 2 years ago (diff)

Moved content of Orange dir to package dir

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