Example #1
0
    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.texd = maxdict(50)  # a cache of tex image rasters
        self._fontd = maxdict(50)

        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__:
            verbose.report(
                'RendererAgg.__init__ width=%s, height=%s' % (width, height),
                'debug-annoying')
        self._renderer = _RendererAgg(int(width),
                                      int(height),
                                      dpi,
                                      debug=False)
        self._filter_renderers = []

        if __debug__:
            verbose.report('RendererAgg.__init__ _RendererAgg done',
                           'debug-annoying')

        self._update_methods()
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__:
            verbose.report('RendererAgg.__init__ done', 'debug-annoying')
Example #2
0
    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.texd = maxdict(50)  # a cache of tex image rasters
        self._fontd = maxdict(50)

        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying')
        self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False)
        if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done',
                                     'debug-annoying')
        #self.draw_path = self._renderer.draw_path  # see below
        self.draw_markers = self._renderer.draw_markers
        self.draw_path_collection = self._renderer.draw_path_collection
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.draw_image = self._renderer.draw_image
        self.copy_from_bbox = self._renderer.copy_from_bbox
        self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__: verbose.report('RendererAgg.__init__ done',
                                     'debug-annoying')
Example #3
0
 def __init__(self, width, height, dpi):
     if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
     RendererBase.__init__(self)
     self.texd = maxdict(50)  # a cache of tex image rasters
     self._fontd = maxdict(50)
     self.dpi = dpi
     self.width = width
     self.height = height
     if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying')
     self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False)
     self._filter_renderers = []
     if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done',
                                  'debug-annoying')
     self._update_methods()
     self.mathtext_parser = MathTextParser('Agg')
     self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
     if __debug__: verbose.report('RendererAgg.__init__ done',
                                  'debug-annoying')
Example #4
0
 def __init__(self):
     """
     Initialization
     """
     self.mathtext_parser = MathTextParser('path')
     self.tex_font_map = None
     from matplotlib.cbook import maxdict
     self._ps_fontd = maxdict(50)
     self._texmanager = None
Example #5
0
    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self.tex_font_map = None

        from matplotlib.cbook import maxdict
        self._ps_fontd = maxdict(50)

        self._texmanager = None

        self._adobe_standard_encoding = None
Example #6
0
    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self.tex_font_map = None

        from matplotlib.cbook import maxdict
        self._ps_fontd = maxdict(50)

        self._texmanager = None

        self._adobe_standard_encoding = None
Example #7
0
    def __init__(self):
        """
        Initialization
        """
        self.mathtext_parser = MathTextParser('path')
        self.tex_font_map = None

        from matplotlib.cbook import maxdict
        self._ps_fontd = maxdict(50)

        self._texmanager = None
Example #8
0
 def __init__(self, ctx, width, height, dpi, fig):
     super().__init__()
     self.fig = fig
     self.ctx = ctx
     self.width = width
     self.height = height
     self.ctx.width = self.width
     self.ctx.height = self.height
     self.dpi = dpi
     self.fontd = maxdict(50)
     self.mathtext_parser = MathTextParser("bitmap")
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """
    debug=1

    # we want to cache the fonts at the class level so that when
    # multiple figures are created we can reuse them.  This helps with
    # a bug on windows where the creation of too many figures leads to
    # too many open file handles.  However, storing them at the class
    # level is not thread safe.  The solution here is to let the
    # FigureCanvas acquire a lock on the fontd at the start of the
    # draw, and release it when it is done.  This allows multiple
    # renderers to share the cached fonts, but only one figure can
    # draw at at time and so the font cache is used by only one
    # renderer at a time

    lock = threading.RLock()
    _fontd = maxdict(50)
    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)

        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying')
        self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False)
        self._filter_renderers = []

        if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done',
                                     'debug-annoying')

        self._update_methods()
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__: verbose.report('RendererAgg.__init__ done',
                                     'debug-annoying')

    def __getstate__(self):
        # We only want to preserve the init keywords of the Renderer.
        # Anything else can be re-created.
        return {'width': self.width, 'height': self.height, 'dpi': self.dpi}

    def __setstate__(self, state):
        self.__init__(state['width'], state['height'], state['dpi'])

    def _get_hinting_flag(self):
        if rcParams['text.hinting']:
            return LOAD_FORCE_AUTOHINT
        else:
            return LOAD_NO_HINTING

    # for filtering to work with rasterization, methods needs to be wrapped.
    # maybe there is better way to do it.
    def draw_markers(self, *kl, **kw):
        return self._renderer.draw_markers(*kl, **kw)

    def draw_path_collection(self, *kl, **kw):
        return self._renderer.draw_path_collection(*kl, **kw)

    def _update_methods(self):
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle
        self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
        self.draw_image = self._renderer.draw_image
        self.copy_from_bbox = self._renderer.copy_from_bbox
        self.get_content_extents = self._renderer.get_content_extents

    def tostring_rgba_minimized(self):
        extents = self.get_content_extents()
        bbox = [[extents[0], self.height - (extents[1] + extents[3])],
                [extents[0] + extents[2], self.height - extents[1]]]
        region = self.copy_from_bbox(bbox)
        return np.array(region), extents

    def draw_path(self, gc, path, transform, rgbFace=None):
        """
        Draw the path
        """
        nmax = rcParams['agg.path.chunksize'] # here at least for testing
        npts = path.vertices.shape[0]
        if (nmax > 100 and npts > nmax and path.should_simplify and
                rgbFace is None and gc.get_hatch() is None):
            nch = np.ceil(npts/float(nmax))
            chsize = int(np.ceil(npts/nch))
            i0 = np.arange(0, npts, chsize)
            i1 = np.zeros_like(i0)
            i1[:-1] = i0[1:] - 1
            i1[-1] = npts
            for ii0, ii1 in zip(i0, i1):
                v = path.vertices[ii0:ii1,:]
                c = path.codes
                if c is not None:
                    c = c[ii0:ii1]
                    c[0] = Path.MOVETO # move to end of last chunk
                p = Path(v, c)
                self._renderer.draw_path(gc, p, transform, rgbFace)
        else:
            self._renderer.draw_path(gc, path, transform, rgbFace)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        if __debug__: verbose.report('RendererAgg.draw_mathtext',
                                     'debug-annoying')
        ox, oy, width, height, descent, font_image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)

        xd = descent * sin(radians(angle))
        yd = descent * cos(radians(angle))
        x = round(x + ox + xd)
        y = round(y - oy + yd)
        self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        """
        Render the text
        """
        if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying')

        if ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)

        flags = get_hinting_flag()
        font = self._get_agg_font(prop)
        if font is None: return None
        if len(s) == 1 and ord(s) > 127:
            font.load_char(ord(s), flags=flags)
        else:
            # We pass '0' for angle here, since it will be rotated (in raster
            # space) in the following call to draw_text_image).
            font.set_text(s, 0, flags=flags)
        font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased'])
        d = font.get_descent() / 64.0
        # The descent needs to be adjusted for the angle
        xo, yo = font.get_bitmap_offset()
        xo /= 64.0
        yo /= 64.0
        xd = -d * sin(radians(angle))
        yd = d * cos(radians(angle))

        #print x, y, int(x), int(y), s
        self._renderer.draw_text_image(
            font, round(x - xd + xo), round(y + yd + yo) + 1, angle, gc)

    def get_text_width_height_descent(self, s, prop, ismath):
        """
        get the width and height in display coords of the string s
        with FontPropertry prop

        # passing rgb is a little hack to make caching in the
        # texmanager more efficient.  It is not meant to be used
        # outside the backend
        """
        if rcParams['text.usetex']:
            # todo: handle props
            size = prop.get_size_in_points()
            texmanager = self.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s, fontsize,
                                                               renderer=self)
            return w, h, d

        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        flags = get_hinting_flag()
        font = self._get_agg_font(prop)
        font.set_text(s, 0.0, flags=flags)  # the width and height of unrotated string
        w, h = font.get_width_height()
        d = font.get_descent()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d /= 64.0
        return w, h, d

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()

        texmanager = self.get_texmanager()

        Z = texmanager.get_grey(s, size, self.dpi)
        Z = np.array(Z * 255.0, np.uint8)

        w, h, d = self.get_text_width_height_descent(s, prop, ismath)
        xd = d * sin(radians(angle))
        yd = d * cos(radians(angle))
        x = round(x + xd)
        y = round(y + yd)

        self._renderer.draw_text_image(Z, x, y, angle, gc)

    def get_canvas_width_height(self):
        'return the canvas width and height in display coords'
        return self.width, self.height

    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, cacheing for efficiency
        """
        if __debug__: verbose.report('RendererAgg._get_agg_font',
                                     'debug-annoying')

        key = hash(prop)
        font = RendererAgg._fontd.get(key)

        if font is None:
            fname = findfont(prop)
            font = RendererAgg._fontd.get(fname)
            if font is None:
                font = FT2Font(
                    fname,
                    hinting_factor=rcParams['text.hinting_factor'])
                RendererAgg._fontd[fname] = font
            RendererAgg._fontd[key] = font

        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, self.dpi)

        return font

    def points_to_pixels(self, points):
        """
        convert point measures to pixes using dpi and the pixels per
        inch of the display
        """
        if __debug__: verbose.report('RendererAgg.points_to_pixels',
                                     'debug-annoying')
        return points*self.dpi/72.0

    def tostring_rgb(self):
        if __debug__: verbose.report('RendererAgg.tostring_rgb',
                                     'debug-annoying')
        return self._renderer.tostring_rgb()

    def tostring_argb(self):
        if __debug__: verbose.report('RendererAgg.tostring_argb',
                                     'debug-annoying')
        return self._renderer.tostring_argb()

    def buffer_rgba(self):
        if __debug__: verbose.report('RendererAgg.buffer_rgba',
                                     'debug-annoying')
        return self._renderer.buffer_rgba()

    def clear(self):
        self._renderer.clear()

    def option_image_nocomposite(self):
        # It is generally faster to composite each image directly to
        # the Figure, and there's no file size benefit to compositing
        # with the Agg backend
        return True

    def option_scale_image(self):
        """
        agg backend support arbitrary scaling of image.
        """
        return True

    def restore_region(self, region, bbox=None, xy=None):
        """
        Restore the saved region. If bbox (instance of BboxBase, or
        its extents) is given, only the region specified by the bbox
        will be restored. *xy* (a tuple of two floasts) optionally
        specifies the new position (the LLC of the original region,
        not the LLC of the bbox) where the region will be restored.

        >>> region = renderer.copy_from_bbox()
        >>> x1, y1, x2, y2 = region.get_extents()
        >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
        ...                         xy=(x1-dx, y1))

        """
        if bbox is not None or xy is not None:
            if bbox is None:
                x1, y1, x2, y2 = region.get_extents()
            elif isinstance(bbox, BboxBase):
                x1, y1, x2, y2 = bbox.extents
            else:
                x1, y1, x2, y2 = bbox

            if xy is None:
                ox, oy = x1, y1
            else:
                ox, oy = xy

            # The incoming data is float, but the _renderer type-checking wants
            # to see integers.
            self._renderer.restore_region(region, int(x1), int(y1),
                                          int(x2), int(y2), int(ox), int(oy))

        else:
            self._renderer.restore_region(region)

    def start_filter(self):
        """
        Start filtering. It simply create a new canvas (the old one is saved).
        """
        self._filter_renderers.append(self._renderer)
        self._renderer = _RendererAgg(int(self.width), int(self.height),
                                      self.dpi)
        self._update_methods()

    def stop_filter(self, post_processing):
        """
        Save the plot in the current canvas as a image and apply
        the *post_processing* function.

           def post_processing(image, dpi):
             # ny, nx, depth = image.shape
             # image (numpy array) has RGBA channels and has a depth of 4.
             ...
             # create a new_image (numpy array of 4 channels, size can be
             # different). The resulting image may have offsets from
             # lower-left corner of the original image
             return new_image, offset_x, offset_y

        The saved renderer is restored and the returned image from
        post_processing is plotted (using draw_image) on it.
        """

        # WARNING.
        # For agg_filter to work, the rendere's method need
        # to overridden in the class. See draw_markers, and draw_path_collections

        from matplotlib._image import fromarray

        width, height = int(self.width), int(self.height)

        buffer, bounds = self.tostring_rgba_minimized()

        l, b, w, h = bounds


        self._renderer = self._filter_renderers.pop()
        self._update_methods()

        if w > 0 and h > 0:
            img = np.fromstring(buffer, np.uint8)
            img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
                                          self.dpi)
            image = fromarray(img, 1)

            gc = self.new_gc()
            self._renderer.draw_image(gc,
                                      l+ox, height - b - h +oy,
                                      image)
Example #10
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

    def __init__(self, width, height, svgwriter, basename=None):
        self.width = width
        self.height = height
        self.writer = XMLWriter(svgwriter)

        self._groupd = {}
        if not rcParams['svg.image_inline']:
            assert basename is not None
            self.basename = basename
            self._imaged = {}
        self._clipd = {}
        self._char_defs = {}
        self._markers = {}
        self._path_collection_id = 0
        self._imaged = {}
        self._hatchd = {}
        self._has_gouraud = False
        self._n_gradients = 0
        self._fonts = {}
        self.mathtext_parser = MathTextParser('SVG')

        RendererBase.__init__(self)
        self._glyph_map = dict()

        svgwriter.write(svgProlog)
        self._start_id = self.writer.start(
            u'svg',
            width=u'%ipt' % width,
            height='%ipt' % height,
            viewBox=u'0 0 %i %i' % (width, height),
            xmlns=u"http://www.w3.org/2000/svg",
            version=u"1.1",
            attrib={u'xmlns:xlink': u"http://www.w3.org/1999/xlink"})
        self._write_default_style()

    def finalize(self):
        self._write_clips()
        self._write_hatches()
        self._write_svgfonts()
        self.writer.close(self._start_id)
        self.writer.flush()

    def _write_default_style(self):
        writer = self.writer
        default_style = generate_css({
            u'stroke-linejoin': u'round',
            u'stroke-linecap': u'square'
        })
        writer.start(u'defs')
        writer.start(u'style', type=u'text/css')
        writer.data(u'*{%s}\n' % default_style)
        writer.end(u'style')
        writer.end(u'defs')

    def _make_id(self, type, content):
        content = str(content)
        if sys.version_info[0] >= 3:
            content = content.encode('utf8')
        return u'%s%s' % (type, md5(content).hexdigest()[:10])

    def _make_flip_transform(self, transform):
        return (transform +
                Affine2D().scale(1.0, -1.0).translate(0.0, self.height))

    def _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _get_hatch(self, gc, rgbFace):
        """
        Create a new hatch pattern
        """
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        edge = gc.get_rgb()
        if edge is not None:
            edge = tuple(edge)
        dictkey = (gc.get_hatch(), rgbFace, edge)
        oid = self._hatchd.get(dictkey)
        if oid is None:
            oid = self._make_id(u'h', dictkey)
            self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid)
        else:
            _, oid = oid
        return oid

    def _write_hatches(self):
        if not len(self._hatchd):
            return
        HATCH_SIZE = 72
        writer = self.writer
        writer.start('defs')
        for ((path, face, stroke), oid) in self._hatchd.values():
            writer.start(u'pattern',
                         id=oid,
                         patternUnits=u"userSpaceOnUse",
                         x=u"0",
                         y=u"0",
                         width=unicode(HATCH_SIZE),
                         height=unicode(HATCH_SIZE))
            path_data = self._convert_path(path,
                                           Affine2D().scale(HATCH_SIZE).scale(
                                               1.0,
                                               -1.0).translate(0, HATCH_SIZE),
                                           simplify=False)
            if face is None:
                fill = u'none'
            else:
                fill = rgb2hex(face)
            writer.element(u'rect',
                           x=u"0",
                           y=u"0",
                           width=unicode(HATCH_SIZE + 1),
                           height=unicode(HATCH_SIZE + 1),
                           fill=fill)
            writer.element(u'path',
                           d=path_data,
                           style=generate_css({
                               u'fill': rgb2hex(stroke),
                               u'stroke': rgb2hex(stroke),
                               u'stroke-width': u'1.0',
                               u'stroke-linecap': u'butt',
                               u'stroke-linejoin': u'miter'
                           }))
            writer.end(u'pattern')
        writer.end(u'defs')

    def _get_style_dict(self, gc, rgbFace):
        """
        return the style string.  style is generated from the
        GraphicsContext and rgbFace
        """
        attrib = {}

        if gc.get_hatch() is not None:
            attrib[u'fill'] = u"url(#%s)" % self._get_hatch(gc, rgbFace)
        else:
            if rgbFace is None:
                attrib[u'fill'] = u'none'
            elif tuple(rgbFace[:3]) != (0, 0, 0):
                attrib[u'fill'] = rgb2hex(rgbFace)

        if gc.get_alpha() != 1.0:
            attrib[u'opacity'] = str(gc.get_alpha())

        offset, seq = gc.get_dashes()
        if seq is not None:
            attrib[u'stroke-dasharray'] = u','.join(
                [u'%f' % val for val in seq])
            attrib[u'stroke-dashoffset'] = unicode(float(offset))

        linewidth = gc.get_linewidth()
        if linewidth:
            attrib[u'stroke'] = rgb2hex(gc.get_rgb())
            if linewidth != 1.0:
                attrib[u'stroke-width'] = str(linewidth)
            if gc.get_joinstyle() != 'round':
                attrib[u'stroke-linejoin'] = gc.get_joinstyle()
            if gc.get_capstyle() != 'projecting':
                attrib[u'stroke-linecap'] = _capstyle_d[gc.get_capstyle()]

        return attrib

    def _get_style(self, gc, rgbFace):
        return generate_css(self._get_style_dict(gc, rgbFace))

    def _get_clip(self, gc):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_trans = gc.get_clip_path()
        if clippath is not None:
            clippath_trans = self._make_flip_transform(clippath_trans)
            dictkey = (id(clippath), str(clippath_trans))
        elif cliprect is not None:
            x, y, w, h = cliprect.bounds
            y = self.height - (y + h)
            dictkey = (x, y, w, h)
        else:
            return None

        clip = self._clipd.get(dictkey)
        if clip is None:
            oid = self._make_id(u'p', dictkey)
            if clippath is not None:
                self._clipd[dictkey] = ((clippath, clippath_trans), oid)
            else:
                self._clipd[dictkey] = (dictkey, oid)
        else:
            clip, oid = clip
        return oid

    def _write_clips(self):
        if not len(self._clipd):
            return
        writer = self.writer
        writer.start('defs')
        for clip, oid in self._clipd.values():
            writer.start('clipPath', id=oid)
            if len(clip) == 2:
                clippath, clippath_trans = clip
                path_data = self._convert_path(clippath,
                                               clippath_trans,
                                               simplify=False)
                writer.element(u'path', d=path_data)
            else:
                x, y, w, h = clip
                writer.element(u'rect',
                               x=unicode(x),
                               y=unicode(y),
                               width=unicode(w),
                               height=unicode(h))
            writer.end(u'clipPath')
        writer.end(u'defs')

    def _write_svgfonts(self):
        if not rcParams['svg.fonttype'] == 'svgfont':
            return

        writer = self.writer
        writer.start(u'defs')
        for font_fname, chars in self._fonts.items():
            font = FT2Font(font_fname)
            font.set_size(72, 72)
            sfnt = font.get_sfnt()
            writer.start(u'font', id=sfnt[(1, 0, 0, 4)])
            writer.element(u'font-face',
                           attrib={
                               u'font-family':
                               font.family_name,
                               u'font-style':
                               font.style_name.lower(),
                               u'units-per-em':
                               u'72',
                               u'bbox':
                               u' '.join(unicode(x / 64.0) for x in font.bbox)
                           })
            for char in chars:
                glyph = font.load_char(char, flags=LOAD_NO_HINTING)
                verts, codes = font.get_path()
                path = Path(verts, codes)
                path_data = self._convert_path(path)
                # name = font.get_glyph_name(char)
                writer.element(
                    u'glyph',
                    d=path_data,
                    attrib={
                        # 'glyph-name': name,
                        u'unicode': unichr(char),
                        u'horiz-adv-x':
                        unicode(glyph.linearHoriAdvance / 65536.0)
                    })
            writer.end(u'font')
        writer.end(u'defs')

    def open_group(self, s, gid=None):
        """
        Open a grouping element with label *s*. If *gid* is given, use
        *gid* as the id of the group.
        """
        if gid:
            self.writer.start('g', id=gid)
        else:
            self._groupd[s] = self._groupd.get(s, 0) + 1
            self.writer.start(u'g', id=u"%s_%d" % (s, self._groupd[s]))

    def close_group(self, s):
        self.writer.end('g')

    def option_image_nocomposite(self):
        """
        if svg.image_noscale is True, compositing multiple images into one is prohibited
        """
        return rcParams['svg.image_noscale']

    def _convert_path(self, path, transform=None, clip=None, simplify=None):
        if clip:
            clip = (0.0, 0.0, self.width, self.height)
        else:
            clip = None
        return _path.convert_to_svg(path, transform, clip, simplify, 6)

    def draw_path(self, gc, path, transform, rgbFace=None):
        trans_and_flip = self._make_flip_transform(transform)
        clip = (rgbFace is None and gc.get_hatch_path() is None)
        simplify = path.should_simplify and clip
        path_data = self._convert_path(path,
                                       trans_and_flip,
                                       clip=clip,
                                       simplify=simplify)

        attrib = {}
        attrib[u'style'] = self._get_style(gc, rgbFace)

        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib[u'clip-path'] = u'url(#%s)' % clipid

        if gc.get_url() is not None:
            self.writer.start(u'a', {u'xlink:href': gc.get_url()})
        self.writer.element(u'path', d=path_data, attrib=attrib)
        if gc.get_url() is not None:
            self.writer.end(u'a')

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        if not len(path.vertices):
            return

        writer = self.writer
        path_data = self._convert_path(marker_path,
                                       marker_trans +
                                       Affine2D().scale(1.0, -1.0),
                                       simplify=False)
        style = self._get_style_dict(gc, rgbFace)
        dictkey = (path_data, generate_css(style))
        oid = self._markers.get(dictkey)
        for key in style.keys():
            if not key.startswith('stroke'):
                del style[key]
        style = generate_css(style)

        if oid is None:
            oid = self._make_id(u'm', dictkey)
            writer.start(u'defs')
            writer.element(u'path', id=oid, d=path_data, style=style)
            writer.end(u'defs')
            self._markers[dictkey] = oid

        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib[u'clip-path'] = u'url(#%s)' % clipid
        writer.start(u'g', attrib=attrib)

        trans_and_flip = self._make_flip_transform(trans)
        attrib = {u'xlink:href': u'#%s' % oid}
        for vertices, code in path.iter_segments(trans_and_flip,
                                                 simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                attrib[u'x'] = unicode(x)
                attrib[u'y'] = unicode(y)
                attrib[u'style'] = self._get_style(gc, rgbFace)
                writer.element(u'use', attrib=attrib)
        writer.end('g')

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):
        writer = self.writer
        path_codes = []
        writer.start(u'defs')
        for i, (path, transform) in enumerate(
                self._iter_collection_raw_paths(master_transform, paths,
                                                all_transforms)):
            transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
            d = self._convert_path(path, transform, simplify=False)
            oid = u'C%x_%x_%s' % (self._path_collection_id, i,
                                  self._make_id(u'', d))
            writer.element(u'path', id=oid, d=d)
            path_codes.append(oid)
        writer.end(u'defs')

        for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
                gc, master_transform, all_transforms, path_codes, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds, urls, offset_position):
            clipid = self._get_clip(gc0)
            url = gc0.get_url()
            if url is not None:
                writer.start(u'a', attrib={u'xlink:href': url})
            if clipid is not None:
                writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid})
            attrib = {
                u'xlink:href': u'#%s' % path_id,
                u'x': unicode(xo),
                u'y': unicode(self.height - yo),
                u'style': self._get_style(gc0, rgbFace)
            }
            writer.element(u'use', attrib=attrib)
            if clipid is not None:
                writer.end(u'g')
            if url is not None:
                writer.end(u'a')

        self._path_collection_id += 1

    def draw_gouraud_triangle(self, gc, points, colors, trans):
        # This uses a method described here:
        #
        #   http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
        #
        # that uses three overlapping linear gradients to simulate a
        # Gouraud triangle.  Each gradient goes from fully opaque in
        # one corner to fully transparent along the opposite edge.
        # The line between the stop points is perpendicular to the
        # opposite edge.  Underlying these three gradients is a solid
        # triangle whose color is the average of all three points.

        writer = self.writer
        if not self._has_gouraud:
            self._has_gouraud = True
            writer.start(u'filter', id=u'colorAdd')
            writer.element(u'feComposite',
                           attrib={u'in': u'SourceGraphic'},
                           in2=u'BackgroundImage',
                           operator=u'arithmetic',
                           k2=u"1",
                           k3=u"1")
            writer.end(u'filter')

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        # Just skip fully-transparent triangles
        if avg_color[-1] == 0.0:
            return

        trans_and_flip = self._make_flip_transform(trans)
        tpoints = trans_and_flip.transform(points)

        writer.start(u'defs')
        for i in range(3):
            x1, y1 = tpoints[i]
            x2, y2 = tpoints[(i + 1) % 3]
            x3, y3 = tpoints[(i + 2) % 3]
            c = colors[i][:]

            if x2 == x3:
                xb = x2
                yb = y1
            elif y2 == y3:
                xb = x1
                yb = y2
            else:
                m1 = (y2 - y3) / (x2 - x3)
                b1 = y2 - (m1 * x2)
                m2 = -(1.0 / m1)
                b2 = y1 - (m2 * x1)
                xb = (-b1 + b2) / (m1 - m2)
                yb = m2 * xb + b2

            writer.start(u'linearGradient',
                         id=u"GR%x_%d" % (self._n_gradients, i),
                         x1=unicode(x1),
                         y1=unicode(y1),
                         x2=unicode(xb),
                         y2=unicode(yb))
            writer.element(u'stop',
                           offset=u'0',
                           style=generate_css({
                               'stop-color': rgb2hex(c),
                               'stop-opacity': unicode(c[-1])
                           }))
            writer.element(u'stop',
                           offset=u'1',
                           style=generate_css({
                               u'stop-color': rgb2hex(c),
                               u'stop-opacity': u"0"
                           }))
            writer.end(u'linearGradient')

        writer.element(u'polygon',
                       id=u'GT%x' % self._n_gradients,
                       points=u" ".join(
                           [unicode(x) for x in x1, y1, x2, y2, x3, y3]))
        writer.end('defs')

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        href = u'#GT%x' % self._n_gradients
        writer.element(u'use',
                       attrib={
                           u'xlink:href': href,
                           u'fill': rgb2hex(avg_color),
                           u'fill-opacity': str(avg_color[-1])
                       })
        for i in range(3):
            writer.element(u'use',
                           attrib={
                               u'xlink:href': href,
                               u'fill':
                               u'url(#GR%x_%d)' % (self._n_gradients, i),
                               u'fill-opacity': u'1',
                               u'filter': u'url(#colorAdd)'
                           })

        self._n_gradients += 1

    def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
                               transform):
        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib[u'clip-path'] = u'url(#%s)' % clipid

        self.writer.start(u'g', attrib=attrib)

        transform = transform.frozen()
        for tri, col in zip(triangles_array, colors_array):
            self.draw_gouraud_triangle(gc, tri, col, transform)

        self.writer.end(u'g')

    def option_scale_image(self):
        return True

    def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None):
        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            # Can't apply clip-path directly to the image because the
            # image has a transformation, which would also be applied
            # to the clip-path
            self.writer.start(u'g',
                              attrib={u'clip-path': u'url(#%s)' % clipid})

        trans = [1, 0, 0, 1, 0, 0]
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            trans[5] = -trans[5]
            attrib[u'transform'] = generate_transform([(u'matrix',
                                                        tuple(trans))])
            assert trans[1] == 0
            assert trans[2] == 0
            numrows, numcols = im.get_size()
            im.reset_matrix()
            im.set_interpolation(0)
            im.resize(numcols, numrows)

        h, w = im.get_size_out()
        oid = getattr(im, '_gid', None)
        url = getattr(im, '_url', None)
        if url is not None:
            self.writer.start(u'a', attrib={u'xlink:href': url})
        if rcParams['svg.image_inline']:
            bytesio = io.BytesIO()
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, bytesio)
            im.flipud_out()
            oid = oid or self._make_id('image', bytesio)
            attrib['xlink:href'] = (
                u"data:image/png;base64,\n" +
                base64.b64encode(bytesio.getvalue()).decode('ascii'))
        else:
            self._imaged[self.basename] = self._imaged.get(self.basename,
                                                           0) + 1
            filename = u'%s.image%d.png' % (self.basename,
                                            self._imaged[self.basename])
            verbose.report('Writing image file for inclusion: %s' % filename)
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, filename)
            im.flipud_out()
            oid = oid or 'Im_' + self._make_id('image', filename)
            attrib[u'xlink:href'] = filename

        alpha = gc.get_alpha()
        if alpha != 1.0:
            attrib['opacity'] = str(alpha)

        attrib['id'] = oid

        if transform is None:
            self.writer.element(u'image',
                                x=unicode(x / trans[0]),
                                y=unicode((self.height - y) / trans[3] - h),
                                width=unicode(w),
                                height=unicode(h),
                                attrib=attrib)
        else:
            flipped = self._make_flip_transform(transform)
            flipped = np.array(flipped.to_values())
            y = y + dy
            if dy > 0.0:
                flipped[3] *= -1.0
                y *= -1.0
            attrib[u'transform'] = generate_transform([(u'matrix', flipped)])
            self.writer.element(u'image',
                                x=unicode(x),
                                y=unicode(y),
                                width=unicode(dx),
                                height=unicode(abs(dy)),
                                attrib=attrib)

        if url is not None:
            self.writer.end(u'a')
        if clipid is not None:
            self.writer.end(u'g')

    def _adjust_char_id(self, char_id):
        return char_id.replace(u"%20", u"_")

    def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
        """
        draw the text by converting them to paths using textpath module.

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. If "TeX", use *usetex* mode.
        """
        writer = self.writer

        writer.comment(s)

        glyph_map = self._glyph_map

        text2path = self._text2path
        color = rgb2hex(gc.get_rgb())
        fontsize = prop.get_size_in_points()

        style = {}
        if color != '#000000':
            style['fill'] = color
        if gc.get_alpha() != 1.0:
            style[u'opacity'] = unicode(gc.get_alpha())

        if not ismath:
            font = text2path._get_font(prop)
            _glyphs = text2path.get_glyphs_with_font(
                font, s, glyph_map=glyph_map, return_new_glyphs_only=True)
            glyph_info, glyph_map_new, rects = _glyphs
            y -= ((font.get_descent() / 64.0) *
                  (prop.get_size_in_points() / text2path.FONT_SCALE))

            if glyph_map_new:
                writer.start(u'defs')
                for char_id, glyph_path in glyph_map_new.iteritems():
                    path = Path(*glyph_path)
                    path_data = self._convert_path(path, simplify=False)
                    writer.element(u'path', id=char_id, d=path_data)
                writer.end(u'defs')

                glyph_map.update(glyph_map_new)

            attrib = {}
            attrib[u'style'] = generate_css(style)
            font_scale = fontsize / text2path.FONT_SCALE
            attrib[u'transform'] = generate_transform([
                (u'translate', (x, y)), (u'rotate', (-angle, )),
                (u'scale', (font_scale, -font_scale))
            ])

            writer.start(u'g', attrib=attrib)
            for glyph_id, xposition, yposition, scale in glyph_info:
                attrib = {u'xlink:href': u'#%s' % glyph_id}
                if xposition != 0.0:
                    attrib[u'x'] = str(xposition)
                if yposition != 0.0:
                    attrib[u'y'] = str(yposition)
                writer.element(u'use', attrib=attrib)

            writer.end(u'g')
        else:
            if ismath == "TeX":
                _glyphs = text2path.get_glyphs_tex(prop,
                                                   s,
                                                   glyph_map=glyph_map,
                                                   return_new_glyphs_only=True)
            else:
                _glyphs = text2path.get_glyphs_mathtext(
                    prop, s, glyph_map=glyph_map, return_new_glyphs_only=True)

            glyph_info, glyph_map_new, rects = _glyphs

            # we store the character glyphs w/o flipping. Instead, the
            # coordinate will be flipped when this characters are
            # used.
            if glyph_map_new:
                writer.start(u'defs')
                for char_id, glyph_path in glyph_map_new.iteritems():
                    char_id = self._adjust_char_id(char_id)
                    # Some characters are blank
                    if not len(glyph_path[0]):
                        path_data = u""
                    else:
                        path = Path(*glyph_path)
                        path_data = self._convert_path(path, simplify=False)
                    writer.element(u'path', id=char_id, d=path_data)
                writer.end(u'defs')

                glyph_map.update(glyph_map_new)

            attrib = {}
            font_scale = fontsize / text2path.FONT_SCALE
            attrib[u'style'] = generate_css(style)
            attrib[u'transform'] = generate_transform([
                (u'translate', (x, y)), (u'rotate', (-angle, )),
                (u'scale', (font_scale, -font_scale))
            ])

            writer.start(u'g', attrib=attrib)
            for char_id, xposition, yposition, scale in glyph_info:
                char_id = self._adjust_char_id(char_id)

                writer.element(u'use',
                               transform=generate_transform([
                                   (u'translate', (xposition, yposition)),
                                   (u'scale', (scale, )),
                               ]),
                               attrib={u'xlink:href': u'#%s' % char_id})

            for verts, codes in rects:
                path = Path(verts, codes)
                path_data = self._convert_path(path, simplify=False)
                writer.element(u'path', d=path_data)

            writer.end('g')

    def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):
        writer = self.writer

        color = rgb2hex(gc.get_rgb())
        style = {}
        if color != '#000000':
            style[u'fill'] = color
        if gc.get_alpha() != 1.0:
            style[u'opacity'] = unicode(gc.get_alpha())

        if not ismath:
            font = self._get_font(prop)
            font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
            y -= font.get_descent() / 64.0

            fontsize = prop.get_size_in_points()

            fontfamily = font.family_name
            fontstyle = prop.get_style()

            attrib = {}
            # Must add "px" to workaround a Firefox bug
            style[u'font-size'] = str(fontsize) + 'px'
            style[u'font-family'] = str(fontfamily)
            style[u'font-style'] = prop.get_style().lower()
            attrib[u'style'] = generate_css(style)

            attrib[u'transform'] = generate_transform([(u'translate', (x, y)),
                                                       (u'rotate', (-angle, ))
                                                       ])

            writer.element(u'text', s, attrib=attrib)

            if rcParams['svg.fonttype'] == 'svgfont':
                fontset = self._fonts.setdefault(font.fname, set())
                for c in s:
                    fontset.add(ord(c))
        else:
            writer.comment(s)

            width, height, descent, svg_elements, used_characters = \
                   self.mathtext_parser.parse(s, 72, prop)
            svg_glyphs = svg_elements.svg_glyphs
            svg_rects = svg_elements.svg_rects

            attrib = {}
            attrib[u'style'] = generate_css(style)
            attrib[u'transform'] = generate_transform([(u'translate', (x, y)),
                                                       (u'rotate', (-angle, ))
                                                       ])

            # Apply attributes to 'g', not 'text', because we likely
            # have some rectangles as well with the same style and
            # transformation
            writer.start(u'g', attrib=attrib)

            writer.start(u'text')

            # Sort the characters by font, and output one tspan for
            # each
            spans = {}
            for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                style = generate_css({
                    u'font-size': unicode(fontsize) + 'px',
                    u'font-family': font.family_name,
                    u'font-style': font.style_name.lower()
                })
                if thetext == 32:
                    thetext = 0xa0  # non-breaking space
                spans.setdefault(style, []).append((new_x, -new_y, thetext))

            if rcParams['svg.fonttype'] == 'svgfont':
                for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                    fontset = self._fonts.setdefault(font.fname, set())
                    fontset.add(thetext)

            for style, chars in spans.items():
                chars.sort()

                same_y = True
                if len(chars) > 1:
                    last_y = chars[0][1]
                    for i in xrange(1, len(chars)):
                        if chars[i][1] != last_y:
                            same_y = False
                            break
                if same_y:
                    ys = unicode(chars[0][1])
                else:
                    ys = ' '.join(unicode(c[1]) for c in chars)

                attrib = {
                    u'style': style,
                    u'x': ' '.join(unicode(c[0]) for c in chars),
                    u'y': ys
                }

                writer.element(u'tspan',
                               u''.join(unichr(c[2]) for c in chars),
                               attrib=attrib)

            writer.end(u'text')

            if len(svg_rects):
                for x, y, width, height in svg_rects:
                    writer.element(u'rect',
                                   x=unicode(x),
                                   y=unicode(-y + height),
                                   width=unicode(width),
                                   height=unicode(height))

            writer.end(u'g')

    def draw_tex(self, gc, x, y, s, prop, angle):
        self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        clipid = self._get_clip(gc)
        if clipid is not None:
            # Cannot apply clip-path directly to the text, because
            # is has a transformation
            self.writer.start(u'g',
                              attrib={u'clip-path': u'url(#%s)' % clipid})

        if rcParams['svg.fonttype'] == 'path':
            self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
        else:
            self._draw_text_as_text(gc, x, y, s, prop, angle, ismath)

        if clipid is not None:
            self.writer.end(u'g')

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        return self._text2path.get_text_width_height_descent(s, prop, ismath)
Example #11
0
class RendererH5Canvas(RendererBase):
    """The renderer handles drawing/rendering operations."""
    fontd = maxdict(50)

    def __init__(self, width, height, ctx, dpi=72):
        self.width = width
        self.height = height
        self.dpi = dpi
        self.ctx = ctx
        self._image_count = 0
        # used to uniquely label each image created in this figure...
        # define the js context
        self.ctx.width = width
        self.ctx.height = height
        #self.ctx.textAlign = "center";
        self.ctx.textBaseline = "alphabetic"
        self.flip = Affine2D().scale(1, -1).translate(0, height)
        self.mathtext_parser = MathTextParser('bitmap')
        self._path_time = 0
        self._text_time = 0
        self._marker_time = 0
        self._sub_time = 0
        self._last_clip = None
        self._last_clip_path = None
        self._clip_count = 0

    def _set_style(self, gc, rgbFace=None):
        ctx = self.ctx
        if rgbFace is not None:
            ctx.fillStyle = mpl_to_css_color(rgbFace, gc.get_alpha())
        ctx.strokeStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha())
        if gc.get_capstyle():
            ctx.lineCap = _capstyle_d[gc.get_capstyle()]
        ctx.lineWidth = self.points_to_pixels(gc.get_linewidth())

    def _path_to_h5(self,
                    ctx,
                    path,
                    transform,
                    clip=None,
                    stroke=True,
                    dashes=(None, None)):
        """Iterate over a path and produce h5 drawing directives."""
        transform = transform + self.flip
        ctx.beginPath()
        current_point = None
        dash_offset, dash_pattern = dashes
        if dash_pattern is not None:
            dash_offset = self.points_to_pixels(dash_offset)
            dash_pattern = tuple(
                [self.points_to_pixels(dash) for dash in dash_pattern])
        for points, code in path.iter_segments(transform, clip=clip):
            # Shift all points by half a pixel, so that integer coordinates are aligned with pixel centers instead of edges
            # This prevents lines that are one pixel wide and aligned with the pixel grid from being rendered as a two-pixel wide line
            # This happens because HTML Canvas defines (0, 0) as the *top left* of a pixel instead of the center,
            # which causes all integer-valued coordinates to fall exactly between pixels
            points += 0.5
            if code == Path.MOVETO:
                ctx.moveTo(points[0], points[1])
                current_point = (points[0], points[1])
            elif code == Path.LINETO:
                t = time.time()
                if (dash_pattern is None) or (current_point is None):
                    ctx.lineTo(points[0], points[1])
                else:
                    dash_offset = ctx.dashedLine(current_point[0],
                                                 current_point[1], points[0],
                                                 points[1],
                                                 (dash_offset, dash_pattern))
                self._sub_time += time.time() - t
                current_point = (points[0], points[1])
            elif code == Path.CURVE3:
                ctx.quadraticCurveTo(*points)
                current_point = (points[2], points[3])
            elif code == Path.CURVE4:
                ctx.bezierCurveTo(*points)
                current_point = (points[4], points[5])
            else:
                pass
        if stroke: ctx.stroke()

    def _do_path_clip(self, ctx, clip):
        self._clip_count += 1
        ctx.save()
        ctx.beginPath()
        ctx.moveTo(clip[0], clip[1])
        ctx.lineTo(clip[2], clip[1])
        ctx.lineTo(clip[2], clip[3])
        ctx.lineTo(clip[0], clip[3])
        ctx.clip()

    def draw_path(self, gc, path, transform, rgbFace=None):
        t = time.time()
        self._set_style(gc, rgbFace)
        clip = self._get_gc_clip_svg(gc)
        clippath, cliptrans = gc.get_clip_path()
        ctx = self.ctx
        if clippath is not None and self._last_clip_path != clippath:
            ctx.restore()
            ctx.save()
            self._path_to_h5(ctx, clippath, cliptrans, None, stroke=False)
            ctx.clip()
            self._last_clip_path = clippath
        if self._last_clip != clip and clip is not None and clippath is None:
            ctx.restore()
            self._do_path_clip(ctx, clip)
            self._last_clip = clip
        if clip is None and clippath is None and (self._last_clip is not None
                                                  or self._last_clip_path
                                                  is not None):
            self._reset_clip()
        if rgbFace is None and gc.get_hatch() is None:
            figure_clip = (0, 0, self.width, self.height)
        else:
            figure_clip = None
        self._path_to_h5(ctx,
                         path,
                         transform,
                         figure_clip,
                         dashes=gc.get_dashes())
        if rgbFace is not None:
            ctx.fill()
            ctx.fillStyle = '#000000'
        self._path_time += time.time() - t

    def _get_gc_clip_svg(self, gc):
        cliprect = gc.get_clip_rectangle()
        if cliprect is not None:
            x, y, w, h = cliprect.bounds
            y = self.height - (y + h)
            return (x, y, x + w, y + h)
        return None

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        t = time.time()
        for vertices, codes in path.iter_segments(trans, simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                self._set_style(gc, rgbFace)
                clip = self._get_gc_clip_svg(gc)
                ctx = self.ctx
                self._path_to_h5(ctx, marker_path,
                                 marker_trans + Affine2D().translate(x, y),
                                 clip)
                if rgbFace is not None:
                    ctx.fill()
                    ctx.fillStyle = '#000000'
        self._marker_time += time.time() - t

    def _slipstream_png(self, x, y, im_buffer, width, height):
        """Insert image directly into HTML canvas as base64-encoded PNG."""
        # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring
        x = math.floor(x + 0.5)
        y = math.floor(y + 1.5)
        # Write the image into a WebPNG object
        f = WebPNG()
        _png.write_png(im_buffer, width, height, f)
        # Write test PNG as file as well
        #_png.write_png(im_buffer, width, height, 'canvas_image_%d.png' % (self._image_count,))
        # Extract the base64-encoded PNG and send it to the canvas
        uname = str(uuid.uuid1()).replace(
            "-", "")  #self.ctx._context_name + str(self._image_count)
        # try to use a unique image name
        enc = "var canvas_image_%s = 'data:image/png;base64,%s';" % (
            uname, f.get_b64())
        s = "function imageLoaded_%s(ev) {\nim = ev.target;\nim_left_to_load_%s -=1;\nif (im_left_to_load_%s == 0) frame_body_%s();\n}\ncanv_im_%s = new Image();\ncanv_im_%s.onload = imageLoaded_%s;\ncanv_im_%s.src = canvas_image_%s;\n" % \
            (uname, self.ctx._context_name, self.ctx._context_name, self.ctx._context_name, uname, uname, uname, uname, uname)
        self.ctx.add_header(enc)
        self.ctx.add_header(s)
        # Once the base64 encoded image has been received, draw it into the canvas
        self.ctx.write("%s.drawImage(canv_im_%s, %g, %g, %g, %g);" %
                       (self.ctx._context_name, uname, x, y, width, height))
        # draw the image as loaded into canv_im_%d...
        self._image_count += 1

    def _reset_clip(self):
        self.ctx.restore()
        self._last_clip = None
        self._last_clip_path = None

    #<1.0.0: def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
    #1.0.0 and up: def draw_image(self, gc, x, y, im, clippath=None):
    #API for draw image changed between 0.99 and 1.0.0
    def draw_image(self, *args, **kwargs):
        x, y, im = args[:3]
        try:
            h, w = im.get_size_out()
        except AttributeError:
            x, y, im = args[1:4]
            h, w = im.get_size_out()
        clippath = (kwargs.has_key('clippath') and kwargs['clippath'] or None)
        if self._last_clip is not None or self._last_clip_path is not None:
            self._reset_clip()
        if clippath is not None:
            self._path_to_h5(self.ctx, clippath, clippath_trans, stroke=False)
            self.ctx.save()
            self.ctx.clip()
        (x, y) = self.flip.transform((x, y))
        im.flipud_out()
        rows, cols, im_buffer = im.as_rgba_str()
        self._slipstream_png(x, (y - h), im_buffer, cols, rows)
        if clippath is not None:
            self.ctx.restore()

    def _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        font.set_size(prop.get_size_in_points(), self.dpi)
        return font

    def draw_tex(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        logger.error(
            "Tex support is currently not implemented. Text element '%s' will not be displayed..."
            % s)

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        if self._last_clip is not None or self._last_clip_path is not None:
            self._reset_clip()
        t = time.time()
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
            return
        angle = math.radians(angle)
        width, height, descent = self.get_text_width_height_descent(
            s, prop, ismath)
        x -= math.sin(angle) * descent
        y -= math.cos(angle) * descent
        ctx = self.ctx
        if angle != 0:
            ctx.save()
            ctx.translate(x, y)
            ctx.rotate(-angle)
            ctx.translate(-x, -y)
        font_size = self.points_to_pixels(prop.get_size_in_points())
        font_str = '%s %s %.3gpx %s, %s' % (prop.get_style(), prop.get_weight(
        ), font_size, prop.get_name(), prop.get_family()[0])
        ctx.font = font_str
        # Set the text color, draw the text and reset the color to black afterwards
        ctx.fillStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha())
        ctx.fillText(unicode(s), x, y)
        ctx.fillStyle = '#000000'
        if angle != 0:
            ctx.restore()
        self._text_time = time.time() - t

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        """Draw math text using matplotlib.mathtext."""
        # Render math string as an image at the configured DPI, and get the image dimensions and baseline depth
        rgba, descent = self.mathtext_parser.to_rgba(
            s,
            color=gc.get_rgb(),
            dpi=self.dpi,
            fontsize=prop.get_size_in_points())
        height, width, tmp = rgba.shape
        angle = math.radians(angle)
        # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring
        x = math.floor(x + 0.5)
        y = math.floor(y + 1.5)
        ctx = self.ctx
        if angle != 0:
            ctx.save()
            ctx.translate(x, y)
            ctx.rotate(-angle)
            ctx.translate(-x, -y)
        # Insert math text image into stream, and adjust x, y reference point to be at top left of image
        self._slipstream_png(x, y - height, rgba.tostring(), width, height)
        if angle != 0:
            ctx.restore()

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath:
            image, d = self.mathtext_parser.parse(s, self.dpi, prop)
            w, h = image.get_width(), image.get_height()
        else:
            font = self._get_font(prop)
            font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
            w, h = font.get_width_height()
            w /= 64.0  # convert from subpixels
            h /= 64.0
            d = font.get_descent() / 64.0
        return w, h, d

    def new_gc(self):
        return GraphicsContextH5Canvas()

    def points_to_pixels(self, points):
        # The standard desktop-publishing (Postscript) point is 1/72 of an inch
        return points / 72.0 * self.dpi
Example #12
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

    def __init__(self, width, height, svgwriter, basename=None):
        self.width = width
        self.height = height
        self._svgwriter = svgwriter

        self._groupd = {}
        if not rcParams['svg.image_inline']:
            assert basename is not None
            self.basename = basename
            self._imaged = {}
        self._clipd = {}
        self._char_defs = {}
        self.mathtext_parser = MathTextParser('SVG')
        svgwriter.write(svgProlog % (width, height, width, height))

    def _draw_svg_element(self, element, details, gc, rgbFace):
        cliprect, clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        style = self._get_style(gc, rgbFace)
        self._svgwriter.write('%s<%s style="%s" %s %s/>\n' %
                              (cliprect, element, style, clippath, details))

    def _path_commands(self, path):
        cmd = []
        while 1:
            code, xp, yp = path.vertex()
            yp = self.height - yp

            if code == agg.path_cmd_stop:
                cmd.append('z')  # Hack, path_cmd_end_poly not found
                break
            elif code == agg.path_cmd_move_to:
                cmd.append('M%g %g' % (xp, yp))
            elif code == agg.path_cmd_line_to:
                cmd.append('L%g %g' % (xp, yp))
            elif code == agg.path_cmd_curve3:
                verts = [xp, yp]
                verts.extend(path.vertex()[1:])
                verts[-1] = self.height - verts[-1]
                cmd.append('Q%g %g %g %g' % tuple(verts))
            elif code == agg.path_cmd_curve4:
                verts = [xp, yp]
                verts.extend(path.vertex()[1:])
                verts[-1] = self.height - verts[-1]
                verts.extend(path.vertex()[1:])
                verts[-1] = self.height - verts[-1]
                cmd.append('C%g %g %g %g %g %g' % tuple(verts))
            elif code == agg.path_cmd_end_poly:
                cmd.append('z')

        path_data = "".join(cmd)
        return path_data

    def _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _get_style(self, gc, rgbFace):
        """
        return the style string.
        style is generated from the GraphicsContext, rgbFace and clippath
        """
        if rgbFace is None:
            fill = 'none'
        else:
            fill = rgb2hex(rgbFace)

        offset, seq = gc.get_dashes()
        if seq is None:
            dashes = ''
        else:
            dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % (
                ','.join(['%f' % val for val in seq]), offset)

        linewidth = gc.get_linewidth()
        if linewidth:
            return 'fill: %s; stroke: %s; stroke-width: %f; ' \
                'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % (
                         fill,
                         rgb2hex(gc.get_rgb()),
                         linewidth,
                         gc.get_joinstyle(),
                         _capstyle_d[gc.get_capstyle()],
                         dashes,
                         gc.get_alpha(),
                )
        else:
            return 'fill: %s; opacity: %f' % (\
                         fill,
                         gc.get_alpha(),
                )

    def _get_gc_clip_svg(self, gc):
        cliprect = gc.get_clip_rectangle()
        clippath = gc.get_clip_path()
        if cliprect is None and clippath is None:
            return '', None
        elif clippath is not None:
            # See if we've already seen this clip rectangle
            key = hash(clippath)
            if self._clipd.get(key) is None:  # If not, store a new clipPath
                self._clipd[key] = clippath
                style = "stroke: gray; fill: none;"
                path_data = self._path_commands(clippath)
                path = """\
<defs>
    <clipPath id="%(key)s">
    <path d="%(path_data)s"/>
    </clipPath>
</defs>
""" % locals()
                return path, key
            else:
                return '', key
        elif cliprect is not None:
            # See if we've already seen this clip rectangle
            key = hash(cliprect)
            if self._clipd.get(key) is None:  # If not, store a new clipPath
                self._clipd[key] = cliprect
                x, y, w, h = cliprect
                y = self.height - (y + h)
                style = "stroke: gray; fill: none;"
                box = """\
<defs>
    <clipPath id="%(key)s">
    <rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"
    style="%(style)s"/>
    </clipPath>
</defs>
""" % locals()
                return box, key
            else:
                # return id of previously defined clipPath
                return '', key

    def open_group(self, s):
        self._groupd[s] = self._groupd.get(s, 0) + 1
        self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s]))

    def close_group(self, s):
        self._svgwriter.write('</g>\n')

    def draw_path(self, gc, rgbFace, path):
        path_data = self._path_commands(path)
        self._draw_svg_element("path", 'd="%s"' % path_data, gc, rgbFace)

    def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2,
                 rotation):
        """
        Ignores angles for now
        """
        details = 'cx="%f" cy="%f" rx="%f" ry="%f" transform="rotate(%1.1f %f %f)"' % \
            (x,  self.height-y, width/2.0, height/2.0, -rotation, x, self.height-y)
        self._draw_svg_element('ellipse', details, gc, rgbFace)

    def option_image_nocomposite(self):
        """
        if svg.image_noscale is True, compositing multiple images into one is prohibited
        """
        return rcParams['svg.image_noscale']

    def draw_image(self, x, y, im, bbox):
        trans = [1, 0, 0, 1, 0, 0]
        transstr = ''
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            if im.get_interpolation() != 0:
                trans[4] += trans[0]
                trans[5] += trans[3]
            trans[5] = -trans[5]
            transstr = 'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans)
            assert trans[1] == 0
            assert trans[2] == 0
            numrows, numcols = im.get_size()
            im.reset_matrix()
            im.set_interpolation(0)
            im.resize(numcols, numrows)

        h, w = im.get_size_out()

        if rcParams['svg.image_inline']:
            filename = os.path.join(tempfile.gettempdir(),
                                    tempfile.gettempprefix() + '.png')

            verbose.report('Writing temporary image file for inlining: %s' %
                           filename)
            # im.write_png() accepts a filename, not file object, would be
            # good to avoid using files and write to mem with StringIO

            # JDH: it *would* be good, but I don't know how to do this
            # since libpng seems to want a FILE* and StringIO doesn't seem
            # to provide one.  I suspect there is a way, but I don't know
            # it

            im.flipud_out()
            im.write_png(filename)
            im.flipud_out()

            imfile = file(filename, 'rb')
            image64 = base64.encodestring(imfile.read())
            imfile.close()
            os.remove(filename)
            hrefstr = 'data:image/png;base64,\n' + image64

        else:
            self._imaged[self.basename] = self._imaged.get(self.basename,
                                                           0) + 1
            filename = '%s.image%d.png' % (self.basename,
                                           self._imaged[self.basename])
            verbose.report('Writing image file for inclusion: %s' % filename)
            im.flipud_out()
            im.write_png(filename)
            im.flipud_out()
            hrefstr = filename

        self._svgwriter.write(
            '<image x="%f" y="%f" width="%f" height="%f" '
            'xlink:href="%s" %s/>\n' %
            (x / trans[0],
             (self.height - y) / trans[3] - h, w, h, hrefstr, transstr))

    def draw_line(self, gc, x1, y1, x2, y2):
        details = 'd="M%f,%fL%f,%f"' % (x1, self.height - y1, x2,
                                        self.height - y2)
        self._draw_svg_element('path', details, gc, None)

    def draw_lines(self, gc, x, y, transform=None):
        if len(x) == 0: return
        if len(x) != len(y):
            raise ValueError('x and y must be the same length')

        y = self.height - y
        details = ['d="M%f,%f' % (x[0], y[0])]
        xys = zip(x[1:], y[1:])
        details.extend(['L%f,%f' % tup for tup in xys])
        details.append('"')
        details = ''.join(details)
        self._draw_svg_element('path', details, gc, None)

    def draw_point(self, gc, x, y):
        # result seems to have a hole in it...
        self.draw_arc(gc, gc.get_rgb(), x, y, 1, 0, 0, 0, 0)

    def draw_polygon(self, gc, rgbFace, points):
        details = 'points = "%s"' % ' '.join(
            ['%f,%f' % (x, self.height - y) for x, y in points])
        self._draw_svg_element('polygon', details, gc, rgbFace)

    def draw_rectangle(self, gc, rgbFace, x, y, width, height):
        details = 'width="%f" height="%f" x="%f" y="%f"' % (
            width, height, x, self.height - y - height)
        self._draw_svg_element('rect', details, gc, rgbFace)

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
            return

        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        y -= font.get_descent() / 64.0

        fontsize = prop.get_size_in_points()
        color = rgb2hex(gc.get_rgb())
        write = self._svgwriter.write

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for c in s:
                path = self._add_char_def(prop, c)
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write('<defs>\n')
                for path in new_chars:
                    write(path)
                write('</defs>\n')

            svg = [
                '<g style="fill: %s; opacity: %f" transform="' %
                (color, gc.get_alpha())
            ]
            if angle != 0:
                svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            elif x != 0 or y != 0:
                svg.append('translate(%f,%f)' % (x, y))
            svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE))

            cmap = font.get_charmap()
            lastgind = None
            currx = 0
            for c in s:
                charnum = self._get_char_def_id(prop, c)
                ccode = ord(c)
                gind = cmap.get(ccode)
                if gind is None:
                    ccode = ord('?')
                    gind = 0
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)

                if lastgind is not None:
                    kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
                else:
                    kern = 0
                lastgind = gind
                currx += kern / 64.0 / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' transform="translate(%f)"' %
                               (currx * (self.FONT_SCALE / fontsize)))
                svg.append('/>\n')
                currx += (glyph.linearHoriAdvance /
                          65536.0) / (self.FONT_SCALE / fontsize)
            svg.append('</g>\n')
            svg = ''.join(svg)
        else:
            thetext = escape_xml_text(s)
            fontfamily = font.family_name
            fontstyle = prop.get_style()

            style = (
                'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f'
                % (fontsize, fontfamily, fontstyle, color, gc.get_alpha()))
            if angle != 0:
                transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (
                    x, y, -angle, -x, -y)
                # Inkscape doesn't support rotate(angle x y)
            else:
                transform = ''

            svg = """\
<text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text>
""" % locals()
        write(svg)

    def _add_char_def(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ord(char)))
        char_num = self._char_defs.get(char_id, None)
        if char_num is not None:
            return None

        path_data = []
        glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING)
        currx, curry = 0.0, 0.0
        for step in glyph.path:
            if step[0] == 0:  # MOVE_TO
                path_data.append("M%f %f" % (step[1], -step[2]))
            elif step[0] == 1:  # LINE_TO
                path_data.append("l%f %f" %
                                 (step[1] - currx, -step[2] - curry))
            elif step[0] == 2:  # CURVE3
                path_data.append("q%f %f %f %f" %
                                 (step[1] - currx, -step[2] - curry,
                                  step[3] - currx, -step[4] - curry))
            elif step[0] == 3:  # CURVE4
                path_data.append(
                    "c%f %f %f %f %f %f" %
                    (step[1] - currx, -step[2] - curry, step[3] - currx,
                     -step[4] - curry, step[5] - currx, -step[6] - curry))
            elif step[0] == 4:  # ENDPOLY
                path_data.append("z")
                currx, curry = 0.0, 0.0

            if step[0] != 4:
                currx, curry = step[-2], -step[-1]
        path_data = ''.join(path_data)
        char_num = 'c_%s' % md5.new(path_data).hexdigest()
        path_element = '<path id="%s" d="%s"/>\n' % (char_num,
                                                     ''.join(path_data))
        self._char_defs[char_id] = char_num
        return path_element

    def _get_char_def_id(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ord(char)))
        return self._char_defs[char_id]

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw math text using matplotlib.mathtext
        """
        width, height, descent, svg_elements, used_characters = \
            self.mathtext_parser.parse(s, 72, prop)
        svg_glyphs = svg_elements.svg_glyphs
        svg_rects = svg_elements.svg_rects
        color = rgb2hex(gc.get_rgb())
        write = self._svgwriter.write

        style = "fill: %s" % color

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                path = self._add_char_def(font, thetext)
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write('<defs>\n')
                for path in new_chars:
                    write(path)
                write('</defs>\n')

            svg = ['<g style="%s" transform="' % style]
            if angle != 0:
                svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append('translate(%f,%f)' % (x, y))
            svg.append('">\n')

            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                charid = self._get_char_def_id(font, thetext)

                svg.append(
                    '<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n'
                    % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE))
            svg.append('</g>\n')
        else:  # not rcParams['svg.embed_char_paths']
            svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)]

            if angle != 0:
                svg.append(
                    ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"'
                    % (x, y, -angle, -x,
                       -y))  # Inkscape doesn't support rotate(angle x y)
            svg.append('>\n')

            curr_x, curr_y = 0.0, 0.0

            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                new_y = -new_y_mtc
                style = "font-size: %f; font-family: %s" % (fontsize,
                                                            font.family_name)

                svg.append('<tspan style="%s"' % style)
                xadvance = metrics.advance
                svg.append(' textLength="%f"' % xadvance)

                dx = new_x - curr_x
                if dx != 0.0:
                    svg.append(' dx="%f"' % dx)

                dy = new_y - curr_y
                if dy != 0.0:
                    svg.append(' dy="%f"' % dy)

                thetext = escape_xml_text(thetext)

                svg.append('>%s</tspan>\n' % thetext)

                curr_x = new_x + xadvance
                curr_y = new_y

            svg.append('</text>\n')

        if len(svg_rects):
            style = "fill: %s; stroke: none" % color
            svg.append('<g style="%s" transform="' % style)
            if angle != 0:
                svg.append('translate(%f,%f) rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append('translate(%f,%f)' % (x, y))
            svg.append('">\n')

            for x, y, width, height in svg_rects:
                svg.append(
                    '<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />'
                    % (x, -y + height, width, height))
            svg.append("</g>")

        self.open_group("mathtext")
        write(''.join(svg))
        self.close_group("mathtext")

    def finish(self):
        write = self._svgwriter.write
        write('</svg>\n')

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath:
            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width, height, descent
        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        w, h = font.get_width_height()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d = font.get_descent()
        d /= 64.0
        return w, h, d
Example #13
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """

    debug=1
    texd = maxdict(50)  # a cache of tex image rasters
    _fontd = maxdict(50)
    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__: verbose.report('RendererAgg.__init__ width=%s, \
                        height=%s'%(width, height), 'debug-annoying')
        self._renderer = _RendererAgg(int(width), int(height), dpi.get(),
                                    debug=False)
        if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done',
                                     'debug-annoying')
        self.draw_polygon = self._renderer.draw_polygon
        self.draw_rectangle = self._renderer.draw_rectangle
        self.draw_path = self._renderer.draw_path
        self.draw_lines = self._renderer.draw_lines
        self.draw_markers = self._renderer.draw_markers
        self.draw_image = self._renderer.draw_image
        self.draw_line_collection = self._renderer.draw_line_collection
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.draw_poly_collection = self._renderer.draw_poly_collection
        self.draw_regpoly_collection = self._renderer.draw_regpoly_collection

        self.copy_from_bbox = self._renderer.copy_from_bbox
        self.restore_region = self._renderer.restore_region
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = lbwh_to_bbox(0,0, self.width, self.height)
        if __debug__: verbose.report('RendererAgg.__init__ done',
                                     'debug-annoying')

    def draw_arc(self, gcEdge, rgbFace, x, y, width, height, angle1, angle2, rotation):
        """
        Draw an arc centered at x,y with width and height and angles
        from 0.0 to 360.0

        If rgbFace is not None, fill the rectangle with that color.  gcEdge
        is a GraphicsContext instance

        Currently, I'm only supporting ellipses, ie angle args are
        ignored
        """
        if __debug__: verbose.report('RendererAgg.draw_arc', 'debug-annoying')
        self._renderer.draw_ellipse(
            gcEdge, rgbFace, x, y, width/2., height/2., rotation)  # ellipse takes radius


    def draw_line(self, gc, x1, y1, x2, y2):
        """
        x and y are equal length arrays, draw lines connecting each
        point in x, y
        """
        if __debug__: verbose.report('RendererAgg.draw_line', 'debug-annoying')
        x = npy.array([x1,x2], float)
        y = npy.array([y1,y2], float)
        self._renderer.draw_lines(gc, x, y)


    def draw_point(self, gc, x, y):
        """
        Draw a single point at x,y
        """
        if __debug__: verbose.report('RendererAgg.draw_point', 'debug-annoying')
        rgbFace = gc.get_rgb()
        self._renderer.draw_ellipse(
            gc, rgbFace, x, y, 0.5, 0.5, 0.0)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        if __debug__: verbose.report('RendererAgg.draw_mathtext',
                                     'debug-annoying')
        ox, oy, width, height, descent, font_image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi.get(), prop)

        x = int(x) + ox
        y = int(y) - oy
        self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
        if 0:
            self._renderer.draw_rectangle(gc, None,
                                          int(x),
                                          self.height-int(y),
                                          width, height)

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        """
        Render the text
        """
        if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying')

        if ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)

        font = self._get_agg_font(prop)
        if font is None: return None
        if len(s) == 1 and ord(s) > 127:
            font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT)
        else:
            font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT)
        font.draw_glyphs_to_bitmap()

        #print x, y, int(x), int(y)

        # We pass '0' for angle here, since is has already been rotated
        # (in vector space) in the above call to font.set_text.
        self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc)


    def get_text_width_height_descent(self, s, prop, ismath):
        """
        get the width and height in display coords of the string s
        with FontPropertry prop

        # passing rgb is a little hack to make cacheing in the
        # texmanager more efficient.  It is not meant to be used
        # outside the backend
        """
        if ismath=='TeX':
            # todo: handle props
            size = prop.get_size_in_points()
            texmanager = self.get_texmanager()
            Z = texmanager.get_grey(s, size, self.dpi.get())
            m,n = Z.shape
            # TODO: descent of TeX text (I am imitating backend_ps here -JKS)
            return n, m, 0

        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi.get(), prop)
            return width, height, descent
        font = self._get_agg_font(prop)
        font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT)  # the width and height of unrotated string
        w, h = font.get_width_height()
        d = font.get_descent()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d /= 64.0
        return w, h, d

    def draw_tex(self, gc, x, y, s, prop, angle):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()
        dpi = self.dpi.get()

        texmanager = self.get_texmanager()
        key = s, size, dpi, angle, texmanager.get_font_config()
        im = self.texd.get(key)
        if im is None:
            Z = texmanager.get_grey(s, size, dpi)
            Z = npy.array(Z * 255.0, npy.uint8)

        self._renderer.draw_text_image(Z, x, y, angle, gc)


    def get_canvas_width_height(self):
        'return the canvas width and height in display coords'
        return self.width, self.height


    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, cacheing for efficiency
        """
        if __debug__: verbose.report('RendererAgg._get_agg_font',
                                     'debug-annoying')

        key = hash(prop)
        font = self._fontd.get(key)

        if font is None:
            fname = findfont(prop)
            font = self._fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self._fontd[fname] = font
            self._fontd[key] = font

        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, self.dpi.get())

        return font


    def points_to_pixels(self, points):
        """
        convert point measures to pixes using dpi and the pixels per
        inch of the display
        """
        if __debug__: verbose.report('RendererAgg.points_to_pixels',
                                     'debug-annoying')
        return points*self.dpi.get()/72.0

    def tostring_rgb(self):
        if __debug__: verbose.report('RendererAgg.tostring_rgb',
                                     'debug-annoying')
        return self._renderer.tostring_rgb()

    def tostring_argb(self):
        if __debug__: verbose.report('RendererAgg.tostring_argb',
                                     'debug-annoying')
        return self._renderer.tostring_argb()

    def buffer_rgba(self,x,y):
        if __debug__: verbose.report('RendererAgg.buffer_rgba',
                                     'debug-annoying')
        return self._renderer.buffer_rgba(x,y)

    def clear(self):
        self._renderer.clear()
Example #14
0
class Path(object):
    """
    :class:`Path` represents a series of possibly disconnected,
    possibly closed, line and curve segments.

    The underlying storage is made up of two parallel numpy arrays:
      - *vertices*: an Nx2 float array of vertices
      - *codes*: an N-length uint8 array of vertex types

    These two arrays always have the same length in the first
    dimension.  For example, to represent a cubic curve, you must
    provide three vertices as well as three codes ``CURVE3``.

    The code types are:

       - ``STOP``   :  1 vertex (ignored)
           A marker for the end of the entire path (currently not
           required and ignored)

       - ``MOVETO`` :  1 vertex
            Pick up the pen and move to the given vertex.

       - ``LINETO`` :  1 vertex
            Draw a line from the current position to the given vertex.

       - ``CURVE3`` :  1 control point, 1 endpoint
          Draw a quadratic Bezier curve from the current position,
          with the given control point, to the given end point.

       - ``CURVE4`` :  2 control points, 1 endpoint
          Draw a cubic Bezier curve from the current position, with
          the given control points, to the given end point.

       - ``CLOSEPOLY`` : 1 vertex (ignored)
          Draw a line segment to the start point of the current
          polyline.

    Users of Path objects should not access the vertices and codes
    arrays directly.  Instead, they should use :meth:`iter_segments`
    to get the vertex/code pairs.  This is important, since many
    :class:`Path` objects, as an optimization, do not store a *codes*
    at all, but have a default one provided for them by
    :meth:`iter_segments`.

    Note also that the vertices and codes arrays should be treated as
    immutable -- there are a number of optimizations and assumptions
    made up front in the constructor that will not change when the
    data changes.
    """

    # Path codes
    STOP = 0  # 1 vertex
    MOVETO = 1  # 1 vertex
    LINETO = 2  # 1 vertex
    CURVE3 = 3  # 2 vertices
    CURVE4 = 4  # 3 vertices
    CLOSEPOLY = 0x4f  # 1 vertex

    NUM_VERTICES = [1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

    code_type = np.uint8

    def __init__(self, vertices, codes=None, _interpolation_steps=1):
        """
        Create a new path with the given vertices and codes.

        *vertices* is an Nx2 numpy float array, masked array or Python
        sequence.

        *codes* is an N-length numpy array or Python sequence of type
        :attr:`matplotlib.path.Path.code_type`.

        These two arrays must have the same length in the first
        dimension.

        If *codes* is None, *vertices* will be treated as a series of
        line segments.

        If *vertices* contains masked values, they will be converted
        to NaNs which are then handled correctly by the Agg
        PathIterator and other consumers of path data, such as
        :meth:`iter_segments`.

        *interpolation_steps* is used as a hint to certain projections,
        such as Polar, that this path should be linearly interpolated
        immediately before drawing.  This attribute is primarily an
        implementation detail and is not intended for public use.
        """
        if ma.isMaskedArray(vertices):
            vertices = vertices.astype(np.float_).filled(np.nan)
        else:
            vertices = np.asarray(vertices, np.float_)

        if codes is not None:
            codes = np.asarray(codes, self.code_type)
            assert codes.ndim == 1
            assert len(codes) == len(vertices)
            if len(codes):
                assert codes[0] == self.MOVETO

        assert vertices.ndim == 2
        assert vertices.shape[1] == 2

        self.should_simplify = (
            rcParams['path.simplify']
            and (len(vertices) >= 128 and
                 (codes is None or np.all(codes <= Path.LINETO))))
        self.simplify_threshold = rcParams['path.simplify_threshold']
        self.has_nonfinite = not np.isfinite(vertices).all()
        self.codes = codes
        self.vertices = vertices
        self._interpolation_steps = _interpolation_steps

    @classmethod
    def make_compound_path_from_polys(cls, XY):
        """
        (static method) Make a compound path object to draw a number
        of polygons with equal numbers of sides XY is a (numpolys x
        numsides x 2) numpy array of vertices.  Return object is a
        :class:`Path`

        .. plot:: mpl_examples/api/histogram_path_demo.py

        """

        # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for the
        # CLOSEPOLY; the vert for the closepoly is ignored but we still need
        # it to keep the codes aligned with the vertices
        numpolys, numsides, two = XY.shape
        assert (two == 2)
        stride = numsides + 1
        nverts = numpolys * stride
        verts = np.zeros((nverts, 2))
        codes = np.ones(nverts, int) * cls.LINETO
        codes[0::stride] = cls.MOVETO
        codes[numsides::stride] = cls.CLOSEPOLY
        for i in range(numsides):
            verts[i::stride] = XY[:, i]

        return cls(verts, codes)

    @classmethod
    def make_compound_path(cls, *args):
        """
        (staticmethod) Make a compound path from a list of Path
        objects.  Only polygons (not curves) are supported.
        """
        for p in args:
            assert p.codes is None

        lengths = [len(x) for x in args]
        total_length = sum(lengths)

        vertices = np.vstack([x.vertices for x in args])
        vertices.reshape((total_length, 2))

        codes = cls.LINETO * np.ones(total_length)
        i = 0
        for length in lengths:
            codes[i] = cls.MOVETO
            i += length

        return cls(vertices, codes)

    def __repr__(self):
        return "Path(%s, %s)" % (self.vertices, self.codes)

    def __len__(self):
        return len(self.vertices)

    def iter_segments(self,
                      transform=None,
                      remove_nans=True,
                      clip=None,
                      snap=False,
                      stroke_width=1.0,
                      simplify=None,
                      curves=True):
        """
        Iterates over all of the curve segments in the path.  Each
        iteration returns a 2-tuple (*vertices*, *code*), where
        *vertices* is a sequence of 1 - 3 coordinate pairs, and *code* is
        one of the :class:`Path` codes.

        Additionally, this method can provide a number of standard
        cleanups and conversions to the path.

        *transform*: if not None, the given affine transformation will
         be applied to the path.

        *remove_nans*: if True, will remove all NaNs from the path and
         insert MOVETO commands to skip over them.

        *clip*: if not None, must be a four-tuple (x1, y1, x2, y2)
         defining a rectangle in which to clip the path.

        *snap*: if None, auto-snap to pixels, to reduce
         fuzziness of rectilinear lines.  If True, force snapping, and
         if False, don't snap.

        *stroke_width*: the width of the stroke being drawn.  Needed
         as a hint for the snapping algorithm.

        *simplify*: if True, perform simplification, to remove
         vertices that do not affect the appearance of the path.  If
         False, perform no simplification.  If None, use the
         should_simplify member variable.

        *curves*: If True, curve segments will be returned as curve
         segments.  If False, all curves will be converted to line
         segments.
        """
        vertices = self.vertices
        if not len(vertices):
            return

        codes = self.codes

        NUM_VERTICES = self.NUM_VERTICES
        MOVETO = self.MOVETO
        LINETO = self.LINETO
        CLOSEPOLY = self.CLOSEPOLY
        STOP = self.STOP

        vertices, codes = cleanup_path(self, transform, remove_nans, clip,
                                       snap, stroke_width, simplify, curves)
        len_vertices = len(vertices)

        i = 0
        while i < len_vertices:
            code = codes[i]
            if code == STOP:
                return
            else:
                num_vertices = NUM_VERTICES[int(code) & 0xf]
                curr_vertices = vertices[i:i + num_vertices].flatten()
                yield curr_vertices, code
                i += num_vertices

    def transformed(self, transform):
        """
        Return a transformed copy of the path.

        .. seealso::

            :class:`matplotlib.transforms.TransformedPath`
                A specialized path class that will cache the
                transformed result and automatically update when the
                transform changes.
        """
        return Path(transform.transform(self.vertices), self.codes,
                    self._interpolation_steps)

    def contains_point(self, point, transform=None, radius=0.0):
        """
        Returns *True* if the path contains the given point.

        If *transform* is not *None*, the path will be transformed
        before performing the test.
        """
        if transform is not None:
            transform = transform.frozen()
        result = point_in_path(point[0], point[1], radius, self, transform)
        return result

    def contains_path(self, path, transform=None):
        """
        Returns *True* if this path completely contains the given path.

        If *transform* is not *None*, the path will be transformed
        before performing the test.
        """
        if transform is not None:
            transform = transform.frozen()
        return path_in_path(self, None, path, transform)

    def get_extents(self, transform=None):
        """
        Returns the extents (*xmin*, *ymin*, *xmax*, *ymax*) of the
        path.

        Unlike computing the extents on the *vertices* alone, this
        algorithm will take into account the curves and deal with
        control points appropriately.
        """
        from transforms import Bbox
        path = self
        if transform is not None:
            transform = transform.frozen()
            if not transform.is_affine:
                path = self.transformed(transform)
                transform = None
        return Bbox(get_path_extents(path, transform))

    def intersects_path(self, other, filled=True):
        """
        Returns *True* if this path intersects another given path.

        *filled*, when True, treats the paths as if they were filled.
        That is, if one path completely encloses the other,
        :meth:`intersects_path` will return True.
        """
        return path_intersects_path(self, other, filled)

    def intersects_bbox(self, bbox, filled=True):
        """
        Returns *True* if this path intersects a given
        :class:`~matplotlib.transforms.Bbox`.

        *filled*, when True, treats the path as if it was filled.
        That is, if one path completely encloses the other,
        :meth:`intersects_path` will return True.
        """
        from transforms import BboxTransformTo
        rectangle = self.unit_rectangle().transformed(BboxTransformTo(bbox))
        result = self.intersects_path(rectangle, filled)
        return result

    def interpolated(self, steps):
        """
        Returns a new path resampled to length N x steps.  Does not
        currently handle interpolating curves.
        """
        if steps == 1:
            return self

        vertices = simple_linear_interpolation(self.vertices, steps)
        codes = self.codes
        if codes is not None:
            new_codes = Path.LINETO * np.ones(((len(codes) - 1) * steps + 1, ))
            new_codes[0::steps] = codes
        else:
            new_codes = None
        return Path(vertices, new_codes)

    def to_polygons(self, transform=None, width=0, height=0):
        """
        Convert this path to a list of polygons.  Each polygon is an
        Nx2 array of vertices.  In other words, each polygon has no
        ``MOVETO`` instructions or curves.  This is useful for
        displaying in backends that do not support compound paths or
        Bezier curves, such as GDK.

        If *width* and *height* are both non-zero then the lines will
        be simplified so that vertices outside of (0, 0), (width,
        height) will be clipped.
        """
        if len(self.vertices) == 0:
            return []

        if transform is not None:
            transform = transform.frozen()

        if self.codes is None and (width == 0 or height == 0):
            if transform is None:
                return [self.vertices]
            else:
                return [transform.transform(self.vertices)]

        # Deal with the case where there are curves and/or multiple
        # subpaths (using extension code)
        return convert_path_to_polygons(self, transform, width, height)

    _unit_rectangle = None

    @classmethod
    def unit_rectangle(cls):
        """
        (staticmethod) Returns a :class:`Path` of the unit rectangle
        from (0, 0) to (1, 1).
        """
        if cls._unit_rectangle is None:
            cls._unit_rectangle = \
                cls([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]],
                    [cls.MOVETO, cls.LINETO, cls.LINETO, cls.LINETO, cls.CLOSEPOLY])
        return cls._unit_rectangle

    _unit_regular_polygons = WeakValueDictionary()

    @classmethod
    def unit_regular_polygon(cls, numVertices):
        """
        (staticmethod) Returns a :class:`Path` for a unit regular
        polygon with the given *numVertices* and radius of 1.0,
        centered at (0, 0).
        """
        if numVertices <= 16:
            path = cls._unit_regular_polygons.get(numVertices)
        else:
            path = None
        if path is None:
            theta = (2 * np.pi / numVertices *
                     np.arange(numVertices + 1).reshape((numVertices + 1, 1)))
            # This initial rotation is to make sure the polygon always
            # "points-up"
            theta += np.pi / 2.0
            verts = np.concatenate((np.cos(theta), np.sin(theta)), 1)
            codes = np.empty((numVertices + 1, ))
            codes[0] = cls.MOVETO
            codes[1:-1] = cls.LINETO
            codes[-1] = cls.CLOSEPOLY
            path = cls(verts, codes)
            if numVertices <= 16:
                cls._unit_regular_polygons[numVertices] = path
        return path

    _unit_regular_stars = WeakValueDictionary()

    @classmethod
    def unit_regular_star(cls, numVertices, innerCircle=0.5):
        """
        (staticmethod) Returns a :class:`Path` for a unit regular star
        with the given numVertices and radius of 1.0, centered at (0,
        0).
        """
        if numVertices <= 16:
            path = cls._unit_regular_stars.get((numVertices, innerCircle))
        else:
            path = None
        if path is None:
            ns2 = numVertices * 2
            theta = (2 * np.pi / ns2 * np.arange(ns2 + 1))
            # This initial rotation is to make sure the polygon always
            # "points-up"
            theta += np.pi / 2.0
            r = np.ones(ns2 + 1)
            r[1::2] = innerCircle
            verts = np.vstack(
                (r * np.cos(theta), r * np.sin(theta))).transpose()
            codes = np.empty((ns2 + 1, ))
            codes[0] = cls.MOVETO
            codes[1:-1] = cls.LINETO
            codes[-1] = cls.CLOSEPOLY
            path = cls(verts, codes)
            if numVertices <= 16:
                cls._unit_regular_polygons[(numVertices, innerCircle)] = path
        return path

    @classmethod
    def unit_regular_asterisk(cls, numVertices):
        """
        (staticmethod) Returns a :class:`Path` for a unit regular
        asterisk with the given numVertices and radius of 1.0,
        centered at (0, 0).
        """
        return cls.unit_regular_star(numVertices, 0.0)

    _unit_circle = None

    @classmethod
    def unit_circle(cls):
        """
        (staticmethod) Returns a :class:`Path` of the unit circle.
        The circle is approximated using cubic Bezier curves.  This
        uses 8 splines around the circle using the approach presented
        here:

          Lancaster, Don.  `Approximating a Circle or an Ellipse Using Four
          Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_.
        """
        if cls._unit_circle is None:
            MAGIC = 0.2652031
            SQRTHALF = np.sqrt(0.5)
            MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0)

            vertices = np.array([[0.0, -1.0], [MAGIC, -1.0],
                                 [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45],
                                 [SQRTHALF, -SQRTHALF],
                                 [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45],
                                 [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC],
                                 [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45],
                                 [SQRTHALF, SQRTHALF],
                                 [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45],
                                 [MAGIC, 1.0], [0.0, 1.0], [-MAGIC, 1.0],
                                 [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45],
                                 [-SQRTHALF, SQRTHALF],
                                 [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45],
                                 [-1.0, MAGIC], [-1.0, 0.0], [-1.0, -MAGIC],
                                 [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45],
                                 [-SQRTHALF, -SQRTHALF],
                                 [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45],
                                 [-MAGIC, -1.0], [0.0, -1.0], [0.0, -1.0]],
                                np.float_)

            codes = cls.CURVE4 * np.ones(26)
            codes[0] = cls.MOVETO
            codes[-1] = cls.CLOSEPOLY

            cls._unit_circle = cls(vertices, codes)
        return cls._unit_circle

    _unit_circle_righthalf = None

    @classmethod
    def unit_circle_righthalf(cls):
        """
        (staticmethod) Returns a :class:`Path` of the right half
        of a unit circle. The circle is approximated using cubic Bezier
        curves.  This uses 4 splines around the circle using the approach
        presented here:

          Lancaster, Don.  `Approximating a Circle or an Ellipse Using Four
          Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_.
        """
        if cls._unit_circle_righthalf is None:
            MAGIC = 0.2652031
            SQRTHALF = np.sqrt(0.5)
            MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0)

            vertices = np.array([[0.0, -1.0], [MAGIC, -1.0],
                                 [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45],
                                 [SQRTHALF, -SQRTHALF],
                                 [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45],
                                 [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC],
                                 [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45],
                                 [SQRTHALF, SQRTHALF],
                                 [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45],
                                 [MAGIC, 1.0], [0.0, 1.0], [0.0, -1.0]],
                                np.float_)

            codes = cls.CURVE4 * np.ones(14)
            codes[0] = cls.MOVETO
            codes[-1] = cls.CLOSEPOLY

            cls._unit_circle_righthalf = cls(vertices, codes)
        return cls._unit_circle_righthalf

    @classmethod
    def arc(cls, theta1, theta2, n=None, is_wedge=False):
        """
        (staticmethod) Returns an arc on the unit circle from angle
        *theta1* to angle *theta2* (in degrees).

        If *n* is provided, it is the number of spline segments to make.
        If *n* is not provided, the number of spline segments is
        determined based on the delta between *theta1* and *theta2*.

           Masionobe, L.  2003.  `Drawing an elliptical arc using
           polylines, quadratic or cubic Bezier curves
           <http://www.spaceroots.org/documents/ellipse/index.html>`_.
        """
        # degrees to radians
        theta1 *= np.pi / 180.0
        theta2 *= np.pi / 180.0

        twopi = np.pi * 2.0
        halfpi = np.pi * 0.5

        eta1 = np.arctan2(np.sin(theta1), np.cos(theta1))
        eta2 = np.arctan2(np.sin(theta2), np.cos(theta2))
        eta2 -= twopi * np.floor((eta2 - eta1) / twopi)
        if (theta2 - theta1 > np.pi) and (eta2 - eta1 < np.pi):
            eta2 += twopi

        # number of curve segments to make
        if n is None:
            n = int(2**np.ceil((eta2 - eta1) / halfpi))
        if n < 1:
            raise ValueError("n must be >= 1 or None")

        deta = (eta2 - eta1) / n
        t = np.tan(0.5 * deta)
        alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0

        steps = np.linspace(eta1, eta2, n + 1, True)
        cos_eta = np.cos(steps)
        sin_eta = np.sin(steps)

        xA = cos_eta[:-1]
        yA = sin_eta[:-1]
        xA_dot = -yA
        yA_dot = xA

        xB = cos_eta[1:]
        yB = sin_eta[1:]
        xB_dot = -yB
        yB_dot = xB

        if is_wedge:
            length = n * 3 + 4
            vertices = np.empty((length, 2), np.float_)
            codes = cls.CURVE4 * np.ones((length, ), cls.code_type)
            vertices[1] = [xA[0], yA[0]]
            codes[0:2] = [cls.MOVETO, cls.LINETO]
            codes[-2:] = [cls.LINETO, cls.CLOSEPOLY]
            vertex_offset = 2
            end = length - 2
        else:
            length = n * 3 + 1
            vertices = np.empty((length, 2), np.float_)
            codes = cls.CURVE4 * np.ones((length, ), cls.code_type)
            vertices[0] = [xA[0], yA[0]]
            codes[0] = cls.MOVETO
            vertex_offset = 1
            end = length

        vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot
        vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot
        vertices[vertex_offset + 1:end:3, 0] = xB - alpha * xB_dot
        vertices[vertex_offset + 1:end:3, 1] = yB - alpha * yB_dot
        vertices[vertex_offset + 2:end:3, 0] = xB
        vertices[vertex_offset + 2:end:3, 1] = yB

        return cls(vertices, codes)

    @classmethod
    def wedge(cls, theta1, theta2, n=None):
        """
        (staticmethod) Returns a wedge of the unit circle from angle
        *theta1* to angle *theta2* (in degrees).

        If *n* is provided, it is the number of spline segments to make.
        If *n* is not provided, the number of spline segments is
        determined based on the delta between *theta1* and *theta2*.
        """
        return cls.arc(theta1, theta2, n, True)

    _hatch_dict = maxdict(8)

    @classmethod
    def hatch(cls, hatchpattern, density=6):
        """
        Given a hatch specifier, *hatchpattern*, generates a Path that
        can be used in a repeated hatching pattern.  *density* is the
        number of lines per unit square.
        """
        from matplotlib.hatch import get_path

        if hatchpattern is None:
            return None

        hatch_path = cls._hatch_dict.get((hatchpattern, density))
        if hatch_path is not None:
            return hatch_path

        hatch_path = get_path(hatchpattern, density)
        cls._hatch_dict[(hatchpattern, density)] = hatch_path
        return hatch_path
Example #15
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

    def __init__(self, width, height, svgwriter, basename=None, image_dpi=72):
        self.width = width
        self.height = height
        self.writer = XMLWriter(svgwriter)
        self.image_dpi = image_dpi  # the actual dpi we want to rasterize stuff with

        self._groupd = {}
        if not rcParams['svg.image_inline']:
            assert basename is not None
            self.basename = basename
            self._imaged = {}
        self._clipd = OrderedDict()
        self._char_defs = {}
        self._markers = {}
        self._path_collection_id = 0
        self._imaged = {}
        self._hatchd = OrderedDict()
        self._has_gouraud = False
        self._n_gradients = 0
        self._fonts = OrderedDict()
        self.mathtext_parser = MathTextParser('SVG')

        RendererBase.__init__(self)
        self._glyph_map = dict()

        svgwriter.write(svgProlog)
        self._start_id = self.writer.start(
            'svg',
            width='%ipt' % width, height='%ipt' % height,
            viewBox='0 0 %i %i' % (width, height),
            xmlns="http://www.w3.org/2000/svg",
            version="1.1",
            attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"})
        self._write_default_style()

    def finalize(self):
        self._write_clips()
        self._write_hatches()
        self._write_svgfonts()
        self.writer.close(self._start_id)
        self.writer.flush()

    def _write_default_style(self):
        writer = self.writer
        default_style = generate_css({
            'stroke-linejoin': 'round',
            'stroke-linecap': 'butt'})
        writer.start('defs')
        writer.start('style', type='text/css')
        writer.data('*{%s}\n' % default_style)
        writer.end('style')
        writer.end('defs')

    def _make_id(self, type, content):
        content = str(content)
        if rcParams['svg.hashsalt'] is None:
            salt = str(uuid.uuid4())
        else:
            salt = rcParams['svg.hashsalt']
        if six.PY3:
            content = content.encode('utf8')
            salt = salt.encode('utf8')
        m = md5()
        m.update(salt)
        m.update(content)
        return '%s%s' % (type, m.hexdigest()[:10])

    def _make_flip_transform(self, transform):
        return (transform +
                Affine2D()
                .scale(1.0, -1.0)
                .translate(0.0, self.height))

    def _get_font(self, prop):
        fname = findfont(prop)
        font = get_font(fname)
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _get_hatch(self, gc, rgbFace):
        """
        Create a new hatch pattern
        """
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        edge = gc.get_rgb()
        if edge is not None:
            edge = tuple(edge)
        dictkey = (gc.get_hatch(), rgbFace, edge)
        oid = self._hatchd.get(dictkey)
        if oid is None:
            oid = self._make_id('h', dictkey)
            self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid)
        else:
            _, oid = oid
        return oid

    def _write_hatches(self):
        if not len(self._hatchd):
            return
        HATCH_SIZE = 72
        writer = self.writer
        writer.start('defs')
        for ((path, face, stroke), oid) in six.itervalues(self._hatchd):
            writer.start(
                'pattern',
                id=oid,
                patternUnits="userSpaceOnUse",
                x="0", y="0", width=six.text_type(HATCH_SIZE),
                height=six.text_type(HATCH_SIZE))
            path_data = self._convert_path(
                path,
                Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE),
                simplify=False)
            if face is None:
                fill = 'none'
            else:
                fill = rgb2hex(face)
            writer.element(
                'rect',
                x="0", y="0", width=six.text_type(HATCH_SIZE+1),
                height=six.text_type(HATCH_SIZE+1),
                fill=fill)
            writer.element(
                'path',
                d=path_data,
                style=generate_css({
                    'fill': rgb2hex(stroke),
                    'stroke': rgb2hex(stroke),
                    'stroke-width': six.text_type(rcParams['hatch.linewidth']),
                    'stroke-linecap': 'butt',
                    'stroke-linejoin': 'miter'
                    })
                )
            writer.end('pattern')
        writer.end('defs')

    def _get_style_dict(self, gc, rgbFace):
        """
        return the style string.  style is generated from the
        GraphicsContext and rgbFace
        """
        attrib = {}

        forced_alpha = gc.get_forced_alpha()

        if gc.get_hatch() is not None:
            attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace)
            if rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha:
                attrib['fill-opacity'] = short_float_fmt(rgbFace[3])
        else:
            if rgbFace is None:
                attrib['fill'] = 'none'
            else:
                if tuple(rgbFace[:3]) != (0, 0, 0):
                    attrib['fill'] = rgb2hex(rgbFace)
                if len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha:
                    attrib['fill-opacity'] = short_float_fmt(rgbFace[3])

        if forced_alpha and gc.get_alpha() != 1.0:
            attrib['opacity'] = short_float_fmt(gc.get_alpha())

        offset, seq = gc.get_dashes()
        if seq is not None:
            attrib['stroke-dasharray'] = ','.join([short_float_fmt(val) for val in seq])
            attrib['stroke-dashoffset'] = short_float_fmt(float(offset))

        linewidth = gc.get_linewidth()
        if linewidth:
            rgb = gc.get_rgb()
            attrib['stroke'] = rgb2hex(rgb)
            if not forced_alpha and rgb[3] != 1.0:
                attrib['stroke-opacity'] = short_float_fmt(rgb[3])
            if linewidth != 1.0:
                attrib['stroke-width'] = short_float_fmt(linewidth)
            if gc.get_joinstyle() != 'round':
                attrib['stroke-linejoin'] = gc.get_joinstyle()
            if gc.get_capstyle() != 'butt':
                attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()]

        return attrib

    def _get_style(self, gc, rgbFace):
        return generate_css(self._get_style_dict(gc, rgbFace))

    def _get_clip(self, gc):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_trans = gc.get_clip_path()
        if clippath is not None:
            clippath_trans = self._make_flip_transform(clippath_trans)
            dictkey = (id(clippath), str(clippath_trans))
        elif cliprect is not None:
            x, y, w, h = cliprect.bounds
            y = self.height-(y+h)
            dictkey = (x, y, w, h)
        else:
            return None

        clip = self._clipd.get(dictkey)
        if clip is None:
            oid = self._make_id('p', dictkey)
            if clippath is not None:
                self._clipd[dictkey] = ((clippath, clippath_trans), oid)
            else:
                self._clipd[dictkey] = (dictkey, oid)
        else:
            clip, oid = clip
        return oid

    def _write_clips(self):
        if not len(self._clipd):
            return
        writer = self.writer
        writer.start('defs')
        for clip, oid in six.itervalues(self._clipd):
            writer.start('clipPath', id=oid)
            if len(clip) == 2:
                clippath, clippath_trans = clip
                path_data = self._convert_path(clippath, clippath_trans, simplify=False)
                writer.element('path', d=path_data)
            else:
                x, y, w, h = clip
                writer.element(
                    'rect',
                    x=short_float_fmt(x),
                    y=short_float_fmt(y),
                    width=short_float_fmt(w),
                    height=short_float_fmt(h))
            writer.end('clipPath')
        writer.end('defs')

    def _write_svgfonts(self):
        if not rcParams['svg.fonttype'] == 'svgfont':
            return

        writer = self.writer
        writer.start('defs')
        for font_fname, chars in six.iteritems(self._fonts):
            font = get_font(font_fname)
            font.set_size(72, 72)
            sfnt = font.get_sfnt()
            writer.start('font', id=sfnt[(1, 0, 0, 4)])
            writer.element(
                'font-face',
                attrib={
                    'font-family': font.family_name,
                    'font-style': font.style_name.lower(),
                    'units-per-em': '72',
                    'bbox': ' '.join(
                        short_float_fmt(x / 64.0) for x in font.bbox)})
            for char in chars:
                glyph = font.load_char(char, flags=LOAD_NO_HINTING)
                verts, codes = font.get_path()
                path = Path(verts, codes)
                path_data = self._convert_path(path)
                # name = font.get_glyph_name(char)
                writer.element(
                    'glyph',
                    d=path_data,
                    attrib={
                        # 'glyph-name': name,
                        'unicode': unichr(char),
                        'horiz-adv-x':
                        short_float_fmt(glyph.linearHoriAdvance / 65536.0)})
            writer.end('font')
        writer.end('defs')

    def open_group(self, s, gid=None):
        """
        Open a grouping element with label *s*. If *gid* is given, use
        *gid* as the id of the group.
        """
        if gid:
            self.writer.start('g', id=gid)
        else:
            self._groupd[s] = self._groupd.get(s, 0) + 1
            self.writer.start('g', id="%s_%d" % (s, self._groupd[s]))

    def close_group(self, s):
        self.writer.end('g')

    def option_image_nocomposite(self):
        """
        return whether to generate a composite image from multiple images on
        a set of axes
        """
        return not rcParams['image.composite_image']

    def _convert_path(self, path, transform=None, clip=None, simplify=None,
                      sketch=None):
        if clip:
            clip = (0.0, 0.0, self.width, self.height)
        else:
            clip = None
        return _path.convert_to_string(
            path, transform, clip, simplify, sketch, 6,
            [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii')

    def draw_path(self, gc, path, transform, rgbFace=None):
        trans_and_flip = self._make_flip_transform(transform)
        clip = (rgbFace is None and gc.get_hatch_path() is None)
        simplify = path.should_simplify and clip
        path_data = self._convert_path(
            path, trans_and_flip, clip=clip, simplify=simplify,
            sketch=gc.get_sketch_params())

        attrib = {}
        attrib['style'] = self._get_style(gc, rgbFace)

        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib['clip-path'] = 'url(#%s)' % clipid

        if gc.get_url() is not None:
            self.writer.start('a', {'xlink:href': gc.get_url()})
        self.writer.element('path', d=path_data, attrib=attrib)
        if gc.get_url() is not None:
            self.writer.end('a')

    def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
        if not len(path.vertices):
            return

        writer = self.writer
        path_data = self._convert_path(
            marker_path,
            marker_trans + Affine2D().scale(1.0, -1.0),
            simplify=False)
        style = self._get_style_dict(gc, rgbFace)
        dictkey = (path_data, generate_css(style))
        oid = self._markers.get(dictkey)
        for key in list(six.iterkeys(style)):
            if not key.startswith('stroke'):
                del style[key]
        style = generate_css(style)

        if oid is None:
            oid = self._make_id('m', dictkey)
            writer.start('defs')
            writer.element('path', id=oid, d=path_data, style=style)
            writer.end('defs')
            self._markers[dictkey] = oid

        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib['clip-path'] = 'url(#%s)' % clipid
        writer.start('g', attrib=attrib)

        trans_and_flip = self._make_flip_transform(trans)
        attrib = {'xlink:href': '#%s' % oid}
        clip = (0, 0, self.width*72, self.height*72)
        for vertices, code in path.iter_segments(
                trans_and_flip, clip=clip, simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                attrib['x'] = short_float_fmt(x)
                attrib['y'] = short_float_fmt(y)
                attrib['style'] = self._get_style(gc, rgbFace)
                writer.element('use', attrib=attrib)
        writer.end('g')

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):
        # Is the optimization worth it? Rough calculation:
        # cost of emitting a path in-line is
        #    (len_path + 5) * uses_per_path
        # cost of definition+use is
        #    (len_path + 3) + 9 * uses_per_path
        len_path = len(paths[0].vertices) if len(paths) > 0 else 0
        uses_per_path = self._iter_collection_uses_per_path(
            paths, all_transforms, offsets, facecolors, edgecolors)
        should_do_optimization = \
            len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path
        if not should_do_optimization:
            return RendererBase.draw_path_collection(
                self, gc, master_transform, paths, all_transforms,
                offsets, offsetTrans, facecolors, edgecolors,
                linewidths, linestyles, antialiaseds, urls,
                offset_position)

        writer = self.writer
        path_codes = []
        writer.start('defs')
        for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
            master_transform, paths, all_transforms)):
            transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
            d = self._convert_path(path, transform, simplify=False)
            oid = 'C%x_%x_%s' % (self._path_collection_id, i,
                                  self._make_id('', d))
            writer.element('path', id=oid, d=d)
            path_codes.append(oid)
        writer.end('defs')

        for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
            gc, master_transform, all_transforms, path_codes, offsets,
            offsetTrans, facecolors, edgecolors, linewidths, linestyles,
            antialiaseds, urls, offset_position):
            clipid = self._get_clip(gc0)
            url = gc0.get_url()
            if url is not None:
                writer.start('a', attrib={'xlink:href': url})
            if clipid is not None:
                writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})
            attrib = {
                'xlink:href': '#%s' % path_id,
                'x': short_float_fmt(xo),
                'y': short_float_fmt(self.height - yo),
                'style': self._get_style(gc0, rgbFace)
                }
            writer.element('use', attrib=attrib)
            if clipid is not None:
                writer.end('g')
            if url is not None:
                writer.end('a')

        self._path_collection_id += 1

    def draw_gouraud_triangle(self, gc, points, colors, trans):
        # This uses a method described here:
        #
        #   http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
        #
        # that uses three overlapping linear gradients to simulate a
        # Gouraud triangle.  Each gradient goes from fully opaque in
        # one corner to fully transparent along the opposite edge.
        # The line between the stop points is perpendicular to the
        # opposite edge.  Underlying these three gradients is a solid
        # triangle whose color is the average of all three points.

        writer = self.writer
        if not self._has_gouraud:
            self._has_gouraud = True
            writer.start(
                'filter',
                id='colorAdd')
            writer.element(
                'feComposite',
                attrib={'in': 'SourceGraphic'},
                in2='BackgroundImage',
                operator='arithmetic',
                k2="1", k3="1")
            writer.end('filter')

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        # Just skip fully-transparent triangles
        if avg_color[-1] == 0.0:
            return

        trans_and_flip = self._make_flip_transform(trans)
        tpoints = trans_and_flip.transform(points)

        writer.start('defs')
        for i in range(3):
            x1, y1 = tpoints[i]
            x2, y2 = tpoints[(i + 1) % 3]
            x3, y3 = tpoints[(i + 2) % 3]
            c = colors[i][:]

            if x2 == x3:
                xb = x2
                yb = y1
            elif y2 == y3:
                xb = x1
                yb = y2
            else:
                m1 = (y2 - y3) / (x2 - x3)
                b1 = y2 - (m1 * x2)
                m2 = -(1.0 / m1)
                b2 = y1 - (m2 * x1)
                xb = (-b1 + b2) / (m1 - m2)
                yb = m2 * xb + b2

            writer.start(
                'linearGradient',
                id="GR%x_%d" % (self._n_gradients, i),
                x1=short_float_fmt(x1), y1=short_float_fmt(y1),
                x2=short_float_fmt(xb), y2=short_float_fmt(yb))
            writer.element(
                'stop',
                offset='0',
                style=generate_css({'stop-color': rgb2hex(c),
                                    'stop-opacity': short_float_fmt(c[-1])}))
            writer.element(
                'stop',
                offset='1',
                style=generate_css({'stop-color': rgb2hex(c),
                                    'stop-opacity': "0"}))
            writer.end('linearGradient')

        writer.element(
            'polygon',
            id='GT%x' % self._n_gradients,
            points=" ".join([short_float_fmt(x)
                             for x in (x1, y1, x2, y2, x3, y3)]))
        writer.end('defs')

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        href = '#GT%x' % self._n_gradients
        writer.element(
            'use',
            attrib={'xlink:href': href,
                    'fill': rgb2hex(avg_color),
                    'fill-opacity': short_float_fmt(avg_color[-1])})
        for i in range(3):
            writer.element(
                'use',
                attrib={'xlink:href': href,
                        'fill': 'url(#GR%x_%d)' % (self._n_gradients, i),
                        'fill-opacity': '1',
                        'filter': 'url(#colorAdd)'})

        self._n_gradients += 1

    def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
                               transform):
        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            attrib['clip-path'] = 'url(#%s)' % clipid

        self.writer.start('g', attrib=attrib)

        transform = transform.frozen()
        for tri, col in zip(triangles_array, colors_array):
            self.draw_gouraud_triangle(gc, tri, col, transform)

        self.writer.end('g')

    def option_scale_image(self):
        return True

    def get_image_magnification(self):
        return self.image_dpi / 72.0

    def draw_image(self, gc, x, y, im, transform=None):
        h, w = im.shape[:2]

        if w == 0 or h == 0:
            return

        attrib = {}
        clipid = self._get_clip(gc)
        if clipid is not None:
            # Can't apply clip-path directly to the image because the
            # image has a transformation, which would also be applied
            # to the clip-path
            self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})

        oid = gc.get_gid()
        url = gc.get_url()
        if url is not None:
            self.writer.start('a', attrib={'xlink:href': url})
        if rcParams['svg.image_inline']:
            bytesio = io.BytesIO()
            _png.write_png(im, bytesio)
            oid = oid or self._make_id('image', bytesio.getvalue())
            attrib['xlink:href'] = (
                "data:image/png;base64,\n" +
                base64.b64encode(bytesio.getvalue()).decode('ascii'))
        else:
            self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1
            filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename])
            verbose.report('Writing image file for inclusion: %s' % filename)
            _png.write_png(im, filename)
            oid = oid or 'Im_' + self._make_id('image', filename)
            attrib['xlink:href'] = filename

        attrib['id'] = oid

        if transform is None:
            w = 72.0 * w / self.image_dpi
            h = 72.0 * h / self.image_dpi

            self.writer.element(
                'image',
                transform=generate_transform([
                    ('scale', (1, -1)), ('translate', (0, -h))]),
                x=short_float_fmt(x),
                y=short_float_fmt(-(self.height - y - h)),
                width=short_float_fmt(w), height=short_float_fmt(h),
                attrib=attrib)
        else:
            alpha = gc.get_alpha()
            if alpha != 1.0:
                attrib['opacity'] = short_float_fmt(alpha)

            flipped = (
                Affine2D().scale(1.0 / w, 1.0 / h) +
                transform +
                Affine2D()
                .translate(x, y)
                .scale(1.0, -1.0)
                .translate(0.0, self.height))

            attrib['transform'] = generate_transform(
                [('matrix', flipped.frozen())])
            self.writer.element(
                'image',
                width=short_float_fmt(w), height=short_float_fmt(h),
                attrib=attrib)

        if url is not None:
            self.writer.end('a')
        if clipid is not None:
            self.writer.end('g')

    def _adjust_char_id(self, char_id):
        return char_id.replace("%20", "_")

    def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
        """
        draw the text by converting them to paths using textpath module.

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. If "TeX", use *usetex* mode.
        """
        writer = self.writer

        writer.comment(s)

        glyph_map=self._glyph_map

        text2path = self._text2path
        color = rgb2hex(gc.get_rgb())
        fontsize = prop.get_size_in_points()

        style = {}
        if color != '#000000':
            style['fill'] = color
        if gc.get_alpha() != 1.0:
            style['opacity'] = short_float_fmt(gc.get_alpha())

        if not ismath:
            font = text2path._get_font(prop)
            _glyphs = text2path.get_glyphs_with_font(
                font, s, glyph_map=glyph_map, return_new_glyphs_only=True)
            glyph_info, glyph_map_new, rects = _glyphs

            if glyph_map_new:
                writer.start('defs')
                for char_id, glyph_path in six.iteritems(glyph_map_new):
                    path = Path(*glyph_path)
                    path_data = self._convert_path(path, simplify=False)
                    writer.element('path', id=char_id, d=path_data)
                writer.end('defs')

                glyph_map.update(glyph_map_new)

            attrib = {}
            attrib['style'] = generate_css(style)
            font_scale = fontsize / text2path.FONT_SCALE
            attrib['transform'] = generate_transform([
                ('translate', (x, y)),
                ('rotate', (-angle,)),
                ('scale', (font_scale, -font_scale))])

            writer.start('g', attrib=attrib)
            for glyph_id, xposition, yposition, scale in glyph_info:
                attrib={'xlink:href': '#%s' % glyph_id}
                if xposition != 0.0:
                    attrib['x'] = short_float_fmt(xposition)
                if yposition != 0.0:
                    attrib['y'] = short_float_fmt(yposition)
                writer.element(
                    'use',
                    attrib=attrib)

            writer.end('g')
        else:
            if ismath == "TeX":
                _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map,
                                                   return_new_glyphs_only=True)
            else:
                _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map,
                                                        return_new_glyphs_only=True)

            glyph_info, glyph_map_new, rects = _glyphs

            # we store the character glyphs w/o flipping. Instead, the
            # coordinate will be flipped when this characters are
            # used.
            if glyph_map_new:
                writer.start('defs')
                for char_id, glyph_path in six.iteritems(glyph_map_new):
                    char_id = self._adjust_char_id(char_id)
                    # Some characters are blank
                    if not len(glyph_path[0]):
                        path_data = ""
                    else:
                        path = Path(*glyph_path)
                        path_data = self._convert_path(path, simplify=False)
                    writer.element('path', id=char_id, d=path_data)
                writer.end('defs')

                glyph_map.update(glyph_map_new)

            attrib = {}
            font_scale = fontsize / text2path.FONT_SCALE
            attrib['style'] = generate_css(style)
            attrib['transform'] = generate_transform([
                ('translate', (x, y)),
                ('rotate', (-angle,)),
                ('scale', (font_scale, -font_scale))])

            writer.start('g', attrib=attrib)
            for char_id, xposition, yposition, scale in glyph_info:
                char_id = self._adjust_char_id(char_id)

                writer.element(
                    'use',
                    transform=generate_transform([
                        ('translate', (xposition, yposition)),
                        ('scale', (scale,)),
                        ]),
                    attrib={'xlink:href': '#%s' % char_id})

            for verts, codes in rects:
                path = Path(verts, codes)
                path_data = self._convert_path(path, simplify=False)
                writer.element('path', d=path_data)

            writer.end('g')

    def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
        writer = self.writer

        color = rgb2hex(gc.get_rgb())
        style = {}
        if color != '#000000':
            style['fill'] = color
        if gc.get_alpha() != 1.0:
            style['opacity'] = short_float_fmt(gc.get_alpha())

        if not ismath:
            font = self._get_font(prop)
            font.set_text(s, 0.0, flags=LOAD_NO_HINTING)

            fontsize = prop.get_size_in_points()

            fontfamily = font.family_name
            fontstyle = prop.get_style()

            attrib = {}
            # Must add "px" to workaround a Firefox bug
            style['font-size'] = short_float_fmt(fontsize) + 'px'
            style['font-family'] = six.text_type(fontfamily)
            style['font-style'] = prop.get_style().lower()
            style['font-weight'] = six.text_type(prop.get_weight()).lower()
            attrib['style'] = generate_css(style)

            if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"):
                # If text anchoring can be supported, get the original
                # coordinates and add alignment information.

                # Get anchor coordinates.
                transform = mtext.get_transform()
                ax, ay = transform.transform_point(mtext.get_position())
                ay = self.height - ay

                # Don't do vertical anchor alignment. Most applications do not
                # support 'alignment-baseline' yet. Apply the vertical layout
                # to the anchor point manually for now.
                angle_rad = angle * np.pi / 180.
                dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)])
                v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
                ax = ax + v_offset * dir_vert[0]
                ay = ay + v_offset * dir_vert[1]

                ha_mpl_to_svg = {'left': 'start', 'right': 'end',
                                 'center': 'middle'}
                style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()]

                attrib['x'] = short_float_fmt(ax)
                attrib['y'] = short_float_fmt(ay)
                attrib['style'] = generate_css(style)
                attrib['transform'] = "rotate(%s, %s, %s)" % (
                    short_float_fmt(-angle),
                    short_float_fmt(ax),
                    short_float_fmt(ay))
                writer.element('text', s, attrib=attrib)
            else:
                attrib['transform'] = generate_transform([
                    ('translate', (x, y)),
                    ('rotate', (-angle,))])

                writer.element('text', s, attrib=attrib)

            if rcParams['svg.fonttype'] == 'svgfont':
                fontset = self._fonts.setdefault(font.fname, set())
                for c in s:
                    fontset.add(ord(c))
        else:
            writer.comment(s)

            width, height, descent, svg_elements, used_characters = \
                   self.mathtext_parser.parse(s, 72, prop)
            svg_glyphs = svg_elements.svg_glyphs
            svg_rects = svg_elements.svg_rects

            attrib = {}
            attrib['style'] = generate_css(style)
            attrib['transform'] = generate_transform([
                ('translate', (x, y)),
                ('rotate', (-angle,))])

            # Apply attributes to 'g', not 'text', because we likely
            # have some rectangles as well with the same style and
            # transformation
            writer.start('g', attrib=attrib)

            writer.start('text')

            # Sort the characters by font, and output one tspan for
            # each
            spans = OrderedDict()
            for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                style = generate_css({
                    'font-size': short_float_fmt(fontsize) + 'px',
                    'font-family': font.family_name,
                    'font-style': font.style_name.lower(),
                    'font-weight': font.style_name.lower()})
                if thetext == 32:
                    thetext = 0xa0 # non-breaking space
                spans.setdefault(style, []).append((new_x, -new_y, thetext))

            if rcParams['svg.fonttype'] == 'svgfont':
                for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                    fontset = self._fonts.setdefault(font.fname, set())
                    fontset.add(thetext)

            for style, chars in six.iteritems(spans):
                chars.sort()

                same_y = True
                if len(chars) > 1:
                    last_y = chars[0][1]
                    for i in xrange(1, len(chars)):
                        if chars[i][1] != last_y:
                            same_y = False
                            break
                if same_y:
                    ys = six.text_type(chars[0][1])
                else:
                    ys = ' '.join(six.text_type(c[1]) for c in chars)

                attrib = {
                    'style': style,
                    'x': ' '.join(short_float_fmt(c[0]) for c in chars),
                    'y': ys
                    }

                writer.element(
                    'tspan',
                    ''.join(unichr(c[2]) for c in chars),
                    attrib=attrib)

            writer.end('text')

            if len(svg_rects):
                for x, y, width, height in svg_rects:
                    writer.element(
                        'rect',
                        x=short_float_fmt(x),
                        y=short_float_fmt(-y + height),
                        width=short_float_fmt(width),
                        height=short_float_fmt(height)
                        )

            writer.end('g')

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        clipid = self._get_clip(gc)
        if clipid is not None:
            # Cannot apply clip-path directly to the text, because
            # is has a transformation
            self.writer.start(
                'g', attrib={'clip-path': 'url(#%s)' % clipid})

        if gc.get_url() is not None:
            self.writer.start('a', {'xlink:href': gc.get_url()})

        if rcParams['svg.fonttype'] == 'path':
            self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext)
        else:
            self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext)

        if gc.get_url() is not None:
            self.writer.end('a')

        if clipid is not None:
            self.writer.end('g')

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        return self._text2path.get_text_width_height_descent(s, prop, ismath)
Example #16
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """
    debug = 1
    texd = maxdict(50)  # a cache of tex image rasters
    _fontd = maxdict(50)

    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__:
            verbose.report(
                'RendererAgg.__init__ width=%s, height=%s' % (width, height),
                'debug-annoying')
        self._renderer = _RendererAgg(int(width),
                                      int(height),
                                      dpi,
                                      debug=False)
        if __debug__:
            verbose.report('RendererAgg.__init__ _RendererAgg done',
                           'debug-annoying')
        #self.draw_path = self._renderer.draw_path  # see below
        self.draw_markers = self._renderer.draw_markers
        self.draw_path_collection = self._renderer.draw_path_collection
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.draw_image = self._renderer.draw_image
        self.copy_from_bbox = self._renderer.copy_from_bbox
        self.restore_region = self._renderer.restore_region
        self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__:
            verbose.report('RendererAgg.__init__ done', 'debug-annoying')

    def draw_path(self, gc, path, transform, rgbFace=None):
        """
        Draw the path
        """
        nmax = rcParams['agg.path.chunksize']  # here at least for testing
        npts = path.vertices.shape[0]
        if (nmax > 100 and npts > nmax and path.should_simplify
                and rgbFace is None and gc.get_hatch() is None):
            nch = npy.ceil(npts / float(nmax))
            chsize = int(npy.ceil(npts / nch))
            i0 = npy.arange(0, npts, chsize)
            i1 = npy.zeros_like(i0)
            i1[:-1] = i0[1:] - 1
            i1[-1] = npts
            for ii0, ii1 in zip(i0, i1):
                v = path.vertices[ii0:ii1, :]
                c = path.codes
                if c is not None:
                    c = c[ii0:ii1]
                    c[0] = Path.MOVETO  # move to end of last chunk
                p = Path(v, c)
                self._renderer.draw_path(gc, p, transform, rgbFace)
        else:
            self._renderer.draw_path(gc, path, transform, rgbFace)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        if __debug__:
            verbose.report('RendererAgg.draw_mathtext', 'debug-annoying')
        ox, oy, width, height, descent, font_image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)

        x = int(x) + ox
        y = int(y) - oy
        self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        """
        Render the text
        """
        if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying')

        if ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)

        font = self._get_agg_font(prop)
        if font is None: return None
        if len(s) == 1 and ord(s) > 127:
            font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT)
        else:
            # We pass '0' for angle here, since it will be rotated (in raster
            # space) in the following call to draw_text_image).
            font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT)
        font.draw_glyphs_to_bitmap()

        #print x, y, int(x), int(y)

        self._renderer.draw_text_image(font.get_image(), int(x),
                                       int(y) + 1, angle, gc)

    def get_text_width_height_descent(self, s, prop, ismath):
        """
        get the width and height in display coords of the string s
        with FontPropertry prop

        # passing rgb is a little hack to make cacheing in the
        # texmanager more efficient.  It is not meant to be used
        # outside the backend
        """
        if ismath == 'TeX':
            # todo: handle props
            size = prop.get_size_in_points()
            texmanager = self.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s,
                                                               fontsize,
                                                               renderer=self)
            return w, h, d

        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
        font = self._get_agg_font(prop)
        font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT
                      )  # the width and height of unrotated string
        w, h = font.get_width_height()
        d = font.get_descent()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d /= 64.0
        return w, h, d

    def draw_tex(self, gc, x, y, s, prop, angle):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()

        texmanager = self.get_texmanager()
        key = s, size, self.dpi, angle, texmanager.get_font_config()
        im = self.texd.get(key)
        if im is None:
            Z = texmanager.get_grey(s, size, self.dpi)
            Z = npy.array(Z * 255.0, npy.uint8)

        self._renderer.draw_text_image(Z, x, y, angle, gc)

    def get_canvas_width_height(self):
        'return the canvas width and height in display coords'
        return self.width, self.height

    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, cacheing for efficiency
        """
        if __debug__:
            verbose.report('RendererAgg._get_agg_font', 'debug-annoying')

        key = hash(prop)
        font = self._fontd.get(key)

        if font is None:
            fname = findfont(prop)
            font = self._fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self._fontd[fname] = font
            self._fontd[key] = font

        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, self.dpi)

        return font

    def points_to_pixels(self, points):
        """
        convert point measures to pixes using dpi and the pixels per
        inch of the display
        """
        if __debug__:
            verbose.report('RendererAgg.points_to_pixels', 'debug-annoying')
        return points * self.dpi / 72.0

    def tostring_rgb(self):
        if __debug__:
            verbose.report('RendererAgg.tostring_rgb', 'debug-annoying')
        return self._renderer.tostring_rgb()

    def tostring_argb(self):
        if __debug__:
            verbose.report('RendererAgg.tostring_argb', 'debug-annoying')
        return self._renderer.tostring_argb()

    def buffer_rgba(self, x, y):
        if __debug__:
            verbose.report('RendererAgg.buffer_rgba', 'debug-annoying')
        return self._renderer.buffer_rgba(x, y)

    def clear(self):
        self._renderer.clear()

    def option_image_nocomposite(self):
        # It is generally faster to composite each image directly to
        # the Figure, and there's no file size benefit to compositing
        # with the Agg backend
        return True
Example #17
0
class RendererGR(RendererBase):
    """
    Handles drawing/rendering operations using GR
    """

    texd = maxdict(50)  # a cache of tex image rasters

    def __init__(self, dpi, width, height):
        self.dpi = dpi
        if __version__[0] >= '2':
            self.nominal_fontsize = 0.001625
            default_dpi = 100
        else:
            self.nominal_fontsize = 0.0013
            default_dpi = 80
        self.width = float(width) * dpi / default_dpi
        self.height = float(height) * dpi / default_dpi
        self.mathtext_parser = MathTextParser('agg')
        self.texmanager = TexManager()

    def configure(self):
        aspect_ratio = self.width / self.height
        if aspect_ratio > 1:
            rect = np.array([0, 1, 0, 1.0 / aspect_ratio])
            self.size = self.width
        else:
            rect = np.array([0, aspect_ratio, 0, 1])
            self.size = self.height
        mwidth, mheight, width, height = gr.inqdspsize()
        if width / (mwidth / 0.0256) < 200:
            mwidth *= self.width / width
            gr.setwsviewport(*rect * mwidth)
        else:
            gr.setwsviewport(*rect * 0.192)
        gr.setwswindow(*rect)
        gr.setviewport(*rect)
        gr.setwindow(0, self.width, 0, self.height)

    def draw_path(self, gc, path, transform, rgbFace=None):
        path = transform.transform_path(path)
        points = path.vertices
        codes = path.codes
        bbox = gc.get_clip_rectangle()
        if bbox is not None and not np.any(np.isnan(bbox.bounds)):
            x, y, w, h = bbox.bounds
            clrt = np.array([x, x + w, y, y + h])
        else:
            clrt = np.array([0, self.width, 0, self.height])
        gr.setviewport(*clrt / self.size)
        gr.setwindow(*clrt)
        if rgbFace is not None and len(points) > 2:
            color = gr.inqcolorfromrgb(rgbFace[0], rgbFace[1], rgbFace[2])
            gr.settransparency(rgbFace[3])
            gr.setcolorrep(color, rgbFace[0], rgbFace[1], rgbFace[2])
            gr.setfillintstyle(gr.INTSTYLE_SOLID)
            gr.setfillcolorind(color)
            gr.drawpath(points, codes, fill=True)
        lw = gc.get_linewidth()
        if lw != 0:
            rgba = gc.get_rgb()[:4]
            color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2])
            gr.settransparency(rgba[3])
            gr.setcolorrep(color, rgba[0], rgba[1], rgba[2])
            if isinstance(gc._linestyle, str):
                gr.setlinetype(linetype[gc._linestyle])
            gr.setlinewidth(lw)
            gr.setlinecolorind(color)
            gr.drawpath(points, codes, fill=False)

    def draw_image(self, gc, x, y, im):
        if hasattr(im, 'as_rgba_str'):
            h, w, s = im.as_rgba_str()
            img = np.fromstring(s, np.uint32)
            img.shape = (h, w)
        elif len(im.shape) == 3 and im.shape[2] == 4 and im.dtype == np.uint8:
            img = im.view(np.uint32)
            img.shape = im.shape[:2]
            h, w = img.shape
        else:
            type_info = repr(type(im))
            if hasattr(im, 'shape'):
                type_info += ' shape=' + repr(im.shape)
            if hasattr(im, 'dtype'):
                type_info += ' dtype=' + repr(im.dtype)
            warnings.warn('Unsupported image type ({}). Please report this at https://github.com/sciapp/python-gr/issues'.format(type_info))
            return
        gr.drawimage(x, x + w, y + h, y, w, h, img)

    def draw_mathtext(self, x, y, angle, Z):
        h, w = Z.shape
        img = np.zeros((h, w), np.uint32)
        for i in range(h):
            for j in range(w):
                img[i, j] = (255 - Z[i, j]) << 24
        a = int(angle)
        if a == 90:
            gr.drawimage(x - h, x, y, y + w, h, w,
                         np.resize(np.rot90(img, 1), (h, w)))
        elif a == 180:
            gr.drawimage(x - w, x, y - h, y, w, h, np.rot90(img, 2))
        elif a == 270:
            gr.drawimage(x, x + h, y - w, y, h, w,
                         np.resize(np.rot90(img, 3), (h, w)))
        else:
            gr.drawimage(x, x + w, y, y + h, w, h, img)

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        size = prop.get_size_in_points()
        key = s, size, self.dpi, angle, self.texmanager.get_font_config()
        im = self.texd.get(key)
        if im is None:
            Z = self.texmanager.get_grey(s, size, self.dpi)
            Z = np.array(255.0 - Z * 255.0, np.uint8)

        self.draw_mathtext(x, y, angle, Z)

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ox, oy, width, height, descent, image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)
        self.draw_mathtext(x, y, angle, 255 - np.asarray(image))

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
            x, y = gr.wctondc(x, y)
            fontsize = prop.get_size_in_points()
            rgba = gc.get_rgb()[:4]
            color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2])
            gr.settransparency(rgba[3])
            gr.setcolorrep(color, rgba[0], rgba[1], rgba[2])
            gr.setcharheight(fontsize * self.nominal_fontsize)
            gr.settextcolorind(color)
            if angle != 0:
                gr.setcharup(-np.sin(angle * np.pi/180),
                             np.cos(angle * np.pi/180))
            else:
                gr.setcharup(0, 1)
            gr.text(x, y, s)

    def flipy(self):
        return False

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath == 'TeX':
            fontsize = prop.get_size_in_points()
            w, h, d = self.texmanager.get_text_width_height_descent(
                s, fontsize, renderer=self)
            return w, h, d
        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
#       family =  prop.get_family()
#       weight = prop.get_weight()
#       style = prop.get_style()
        fontsize = prop.get_size_in_points()
        gr.setcharheight(fontsize * self.nominal_fontsize)
        gr.setcharup(0, 1)
        (tbx, tby) = gr.inqtextext(0, 0, s)
        width, height, descent = tbx[1], tby[2], 0.2 * tby[2]
        return width, height, descent

    def new_gc(self):
        return GraphicsContextGR()

    def points_to_pixels(self, points):
        return points
Example #18
0
class RendererGR(RendererBase):
    """
    Handles drawing/rendering operations using GR
    """

    texd = maxdict(50)  # a cache of tex image rasters

    def __init__(self, dpi):
        self.dpi = dpi
        self.width = 640.0 * dpi / 80
        self.height = 480.0 * dpi / 80
        mwidth, mheight, width, height = gr.inqdspsize()
        if (width / (mwidth / 0.0256) < 200):
            mwidth *= self.width / width
            gr.setwsviewport(0, mwidth, 0, mwidth * 0.75)
        else:
            gr.setwsviewport(0, 0.192, 0, 0.144)
        gr.setwswindow(0, 1, 0, 0.75)
        gr.setviewport(0, 1, 0, 0.75)
        gr.setwindow(0, self.width, 0, self.height)
        self.mathtext_parser = MathTextParser('agg')
        self.texmanager = TexManager()

    def draw_path(self, gc, path, transform, rgbFace=None):
        path = transform.transform_path(path)
        points = path.vertices
        codes = path.codes
        bbox = gc.get_clip_rectangle()
        if bbox is not None:
            x, y, w, h = bbox.bounds
            clrt = np.array([x, x + w, y, y + h])
        else:
            clrt = np.array([0, self.width, 0, self.height])
        gr.setviewport(*clrt/self.width)
        gr.setwindow(*clrt)
        if rgbFace is not None and len(points) > 2:
            color = gr.inqcolorfromrgb(rgbFace[0], rgbFace[1], rgbFace[2])
            gr.settransparency(rgbFace[3])
            gr.setcolorrep(color, rgbFace[0], rgbFace[1], rgbFace[2])
            gr.setfillintstyle(gr.INTSTYLE_SOLID)
            gr.setfillcolorind(color)
            gr.drawpath(points, codes, fill=True)
        lw = gc.get_linewidth()
        if lw != 0:
            rgba = gc.get_rgb()[:4]
            color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2])
            gr.settransparency(rgba[3])
            gr.setcolorrep(color, rgba[0], rgba[1], rgba[2])
            if type(gc._linestyle) is unicode:
                gr.setlinetype(linetype[gc._linestyle])
            gr.setlinewidth(lw)
            gr.setlinecolorind(color)
            gr.drawpath(points, codes, fill=False)

    def draw_image(self, gc, x, y, im):
        h, w, s = im.as_rgba_str()
        img = np.fromstring(s, np.uint32)
        img.shape = (h, w)
        gr.drawimage(x, x + w, y + h, y, w, h, img)

    def draw_mathtext(self, x, y, angle, Z):
        h, w = Z.shape
        img = np.zeros((h, w), np.uint32)
        for i in range(h):
            for j in range(w):
                img[i, j] = (255 - Z[i, j]) << 24
        a = int(angle)
        if a == 90:
            gr.drawimage(x - h, x, y, y + w, h, w,
                         np.resize(np.rot90(img, 1), (h, w)))
        elif a == 180:
            gr.drawimage(x - w, x, y - h, y, w, h, np.rot90(img, 2))
        elif a == 270:
            gr.drawimage(x, x + h, y - w, y, h, w,
                         np.resize(np.rot90(img, 3), (h, w)))
        else:
            gr.drawimage(x, x + w, y, y + h, w, h, img)

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        size = prop.get_size_in_points()
        key = s, size, self.dpi, angle, self.texmanager.get_font_config()
        im = self.texd.get(key)
        if im is None:
            Z = self.texmanager.get_grey(s, size, self.dpi)
            Z = np.array(255.0 - Z * 255.0, np.uint8)

        self.draw_mathtext(x, y, angle, Z)

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ox, oy, width, height, descent, image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)
        self.draw_mathtext(x, y, angle, 255 - image.as_array())

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
            x, y = gr.wctondc(x, y)
            s = s.replace(u'\u2212', '-')
            fontsize = prop.get_size_in_points()
            rgba = gc.get_rgb()[:4]
            color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2])
            gr.settransparency(rgba[3])
            gr.setcolorrep(color, rgba[0], rgba[1], rgba[2])
            gr.setcharheight(fontsize * 0.0013)
            gr.settextcolorind(color)
            if angle != 0:
                gr.setcharup(-np.sin(angle * np.pi/180),
                             np.cos(angle * np.pi/180))
            else:
                gr.setcharup(0, 1)
            gr.text(x, y, s.encode("latin-1"))

    def flipy(self):
        return False

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath == 'TeX':
            fontsize = prop.get_size_in_points()
            w, h, d = self.texmanager.get_text_width_height_descent(
                s, fontsize, renderer=self)
            return w, h, d
        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
#       family =  prop.get_family()
#       weight = prop.get_weight()
#       style = prop.get_style()
        s = s.replace(u'\u2212', '-').encode("latin-1")
        fontsize = prop.get_size_in_points()
        gr.setcharheight(fontsize * 0.0013)
        gr.setcharup(0, 1)
        (tbx, tby) = gr.inqtextext(0, 0, s)
        width, height, descent = tbx[1], tby[2], 0
        return width, height, descent

    def new_gc(self):
        return GraphicsContextGR()

    def points_to_pixels(self, points):
        return points
Example #19
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

    def __init__(self, width, height, svgwriter, basename=None):
        self.width = width
        self.height = height
        self._svgwriter = svgwriter

        self._groupd = {}
        if not rcParams['svg.image_inline']:
            assert basename is not None
            self.basename = basename
            self._imaged = {}
        self._clipd = {}
        self._char_defs = {}
        self._markers = {}
        self._path_collection_id = 0
        self._imaged = {}
        self._hatchd = {}
        self._n_gradients = 0
        self.mathtext_parser = MathTextParser('SVG')

        RendererBase.__init__(self)
        self._glyph_map = dict()

        svgwriter.write(svgProlog % (width, height, width, height))

    def _draw_svg_element(self, element, details, gc, rgbFace):
        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = u''
        else:
            clippath = u'clip-path="url(#%s)"' % clipid

        if gc.get_url() is not None:
            self._svgwriter.write(u'<a xlink:href="%s">' % gc.get_url())
        style = self._get_style(gc, rgbFace)
        self._svgwriter.write(u'<%s style="%s" %s %s/>\n' %
                              (element, style, clippath, details))
        if gc.get_url() is not None:
            self._svgwriter.write(u'</a>')

    def _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _get_hatch(self, gc, rgbFace):
        """
        Create a new hatch pattern
        """
        HATCH_SIZE = 72
        dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb())
        id = self._hatchd.get(dictkey)
        if id is None:
            id = 'h%s' % md5(unicode(dictkey).encode('ascii')).hexdigest()
            self._svgwriter.write(u'<defs>\n  <pattern id="%s" ' % id)
            self._svgwriter.write(
                u'patternUnits="userSpaceOnUse" x="0" y="0" ')
            self._svgwriter.write(u' width="%d" height="%d" >\n' %
                                  (HATCH_SIZE, HATCH_SIZE))
            path_data = self._convert_path(gc.get_hatch_path(),
                                           Affine2D().scale(HATCH_SIZE).scale(
                                               1.0,
                                               -1.0).translate(0, HATCH_SIZE),
                                           simplify=False)
            if rgbFace is None:
                fill = 'none'
            else:
                fill = rgb2hex(rgbFace)
            self._svgwriter.write(
                u'<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' %
                (HATCH_SIZE + 1, HATCH_SIZE + 1, fill))
            path = u'<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % (
                path_data, rgb2hex(gc.get_rgb()), rgb2hex(gc.get_rgb()))
            self._svgwriter.write(path)
            self._svgwriter.write(u'\n  </pattern>\n</defs>')
            self._hatchd[dictkey] = id
        return id

    def _get_style(self, gc, rgbFace):
        """
        return the style string.
        style is generated from the GraphicsContext, rgbFace and clippath
        """
        if gc.get_hatch() is not None:
            fill = u"url(#%s)" % self._get_hatch(gc, rgbFace)
        else:
            if rgbFace is None:
                fill = u'none'
            else:
                fill = rgb2hex(rgbFace)

        offset, seq = gc.get_dashes()
        if seq is None:
            dashes = u''
        else:
            dashes = u'stroke-dasharray: %s; stroke-dashoffset: %f;' % (
                u','.join([u'%f' % val for val in seq]), offset)

        linewidth = gc.get_linewidth()
        if linewidth:
            return u'fill: %s; stroke: %s; stroke-width: %f; ' \
                u'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % (
                         fill,
                         rgb2hex(gc.get_rgb()),
                         linewidth,
                         gc.get_joinstyle(),
                         _capstyle_d[gc.get_capstyle()],
                         dashes,
                         gc.get_alpha(),
                )
        else:
            return u'fill: %s; opacity: %f' % (\
                         fill,
                         gc.get_alpha(),
                )

    def _get_gc_clip_svg(self, gc):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_trans = gc.get_clip_path()
        if clippath is not None:
            clippath_trans = self._make_flip_transform(clippath_trans)
            path_data = self._convert_path(clippath,
                                           clippath_trans,
                                           simplify=False)
            path = u'<path d="%s"/>' % path_data
        elif cliprect is not None:
            x, y, w, h = cliprect.bounds
            y = self.height - (y + h)
            path = u'<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals(
            )
        else:
            return None

        id = self._clipd.get(path)
        if id is None:
            id = u'p%s' % md5(path.encode('ascii')).hexdigest()
            self._svgwriter.write(u'<defs>\n  <clipPath id="%s">\n' % id)
            self._svgwriter.write(path)
            self._svgwriter.write(u'\n  </clipPath>\n</defs>')
            self._clipd[path] = id
        return id

    def open_group(self, s, gid=None):
        """
        Open a grouping element with label *s*. If *gid* is given, use
        *gid* as the id of the group.
        """
        if gid:
            self._svgwriter.write(u'<g id="%s">\n' % (gid))
        else:
            self._groupd[s] = self._groupd.get(s, 0) + 1
            self._svgwriter.write(u'<g id="%s%d">\n' % (s, self._groupd[s]))

    def close_group(self, s):
        self._svgwriter.write(u'</g>\n')

    def option_image_nocomposite(self):
        """
        if svg.image_noscale is True, compositing multiple images into one is prohibited
        """
        return rcParams['svg.image_noscale']

    _path_commands = {
        Path.MOVETO: u'M%f %f',
        Path.LINETO: u'L%f %f',
        Path.CURVE3: u'Q%f %f %f %f',
        Path.CURVE4: u'C%f %f %f %f %f %f'
    }

    def _make_flip_transform(self, transform):
        return (transform +
                Affine2D().scale(1.0, -1.0).translate(0.0, self.height))

    def _convert_path(self, path, transform, clip=False, simplify=None):
        path_data = []
        appender = path_data.append
        path_commands = self._path_commands
        currpos = 0
        if clip:
            clip = (0.0, 0.0, self.width, self.height)
        else:
            clip = None
        for points, code in path.iter_segments(transform,
                                               clip=clip,
                                               simplify=simplify):
            if code == Path.CLOSEPOLY:
                segment = u'z'
            else:
                segment = path_commands[code] % tuple(points)

            if currpos + len(segment) > 75:
                appender(u"\n")
                currpos = 0
            appender(segment)
            currpos += len(segment)
        return u''.join(path_data)

    def draw_path(self, gc, path, transform, rgbFace=None):
        trans_and_flip = self._make_flip_transform(transform)
        clip = (rgbFace is None and gc.get_hatch_path() is None)
        simplify = path.should_simplify and clip
        path_data = self._convert_path(path,
                                       trans_and_flip,
                                       clip=clip,
                                       simplify=simplify)
        self._draw_svg_element(u'path', u'd="%s"' % path_data, gc, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        write = self._svgwriter.write

        key = self._convert_path(marker_path,
                                 marker_trans + Affine2D().scale(1.0, -1.0),
                                 simplify=False)
        name = self._markers.get(key)
        if name is None:
            name = u'm%s' % md5(key.encode('ascii')).hexdigest()
            write(u'<defs><path id="%s" d="%s"/></defs>\n' % (name, key))
            self._markers[key] = name

        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = u''
        else:
            clippath = u'clip-path="url(#%s)"' % clipid

        write(u'<g %s>' % clippath)
        trans_and_flip = self._make_flip_transform(trans)
        for vertices, code in path.iter_segments(trans_and_flip,
                                                 simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                details = u'xlink:href="#%s" x="%f" y="%f"' % (name, x, y)
                style = self._get_style(gc, rgbFace)
                self._svgwriter.write(u'<use style="%s" %s/>\n' %
                                      (style, details))
        write(u'</g>')

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls):
        write = self._svgwriter.write

        path_codes = []
        write(u'<defs>\n')
        for i, (path, transform) in enumerate(
                self._iter_collection_raw_paths(master_transform, paths,
                                                all_transforms)):
            transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
            d = self._convert_path(path, transform, simplify=False)
            name = u'coll%x_%x_%s' % (self._path_collection_id, i,
                                      md5(d.encode('ascii')).hexdigest())
            write(u'<path id="%s" d="%s"/>\n' % (name, d))
            path_codes.append(name)
        write(u'</defs>\n')

        for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
                gc, path_codes, offsets, offsetTrans, facecolors, edgecolors,
                linewidths, linestyles, antialiaseds, urls):
            clipid = self._get_gc_clip_svg(gc0)
            url = gc0.get_url()
            if url is not None:
                self._svgwriter.write(u'<a xlink:href="%s">' % url)
            if clipid is not None:
                write(u'<g clip-path="url(#%s)">' % clipid)
            details = u'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo,
                                                           self.height - yo)
            style = self._get_style(gc0, rgbFace)
            self._svgwriter.write(u'<use style="%s" %s/>\n' % (style, details))
            if clipid is not None:
                write(u'</g>')
            if url is not None:
                self._svgwriter.write(u'</a>')

        self._path_collection_id += 1

    def draw_gouraud_triangle(self, gc, points, colors, trans):
        # This uses a method described here:
        #
        #   http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
        #
        # that uses three overlapping linear gradients to simulate a
        # Gouraud triangle.  Each gradient goes from fully opaque in
        # one corner to fully transparent along the opposite edge.
        # The line between the stop points is perpendicular to the
        # opposite edge.  Underlying these three gradients is a solid
        # triangle whose color is the average of all three points.

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        # Just skip fully-transparent triangles
        if avg_color[-1] == 0.0:
            return

        trans_and_flip = self._make_flip_transform(trans)
        tpoints = trans_and_flip.transform(points)
        write = self._svgwriter.write

        write(u'<defs>')
        for i in range(3):
            x1, y1 = points[i]
            x2, y2 = points[(i + 1) % 3]
            x3, y3 = points[(i + 2) % 3]
            c = colors[i][:]

            if x2 == x3:
                xb = x2
                yb = y1
            elif y2 == y3:
                xb = x1
                yb = y2
            else:
                m1 = (y2 - y3) / (x2 - x3)
                b1 = y2 - (m1 * x2)
                m2 = -(1.0 / m1)
                b2 = y1 - (m2 * x1)
                xb = (-b1 + b2) / (m1 - m2)
                yb = m2 * xb + b2

            write(
                u'<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">'
                % (self._n_gradients, i, x1, y1, xb, yb))
            write(u'<stop offset="0" style="stop-color:%s;stop-opacity:%f"/>' %
                  (rgb2hex(c), c[-1]))
            write(u'<stop offset="1" style="stop-color:%s;stop-opacity:0"/>' %
                  rgb2hex(c))
            write(u'</linearGradient>')

        # Define the triangle itself as a "def" since we use it 4 times
        write(u'<polygon id="GT%x" points="%f %f %f %f %f %f"/>' %
              (self._n_gradients, x1, y1, x2, y2, x3, y3))
        write(u'</defs>\n')

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        write(u'<use xlink:href="#GT%x" fill="%s" fill-opacity="%f"/>\n' %
              (self._n_gradients, rgb2hex(avg_color), avg_color[-1]))
        for i in range(3):
            write(
                u'<use xlink:href="#GT%x" fill="url(#GR%x_%d)" fill-opacity="1" filter="url(#colorAdd)"/>\n'
                % (self._n_gradients, self._n_gradients, i))

        self._n_gradients += 1

    def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
                               transform):
        write = self._svgwriter.write

        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = u''
        else:
            clippath = u'clip-path="url(#%s)"' % clipid

        write(u'<g %s>\n' % clippath)

        transform = transform.frozen()
        for tri, col in zip(triangles_array, colors_array):
            self.draw_gouraud_triangle(gc, tri, col, transform)

        write(u'</g>\n')

    def draw_image(self, gc, x, y, im):
        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = u''
        else:
            clippath = u'clip-path="url(#%s)"' % clipid

        trans = [1, 0, 0, 1, 0, 0]
        transstr = u''
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            trans[5] = -trans[5]
            transstr = u'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans)
            assert trans[1] == 0
            assert trans[2] == 0
            numrows, numcols = im.get_size()
            im.reset_matrix()
            im.set_interpolation(0)
            im.resize(numcols, numrows)

        h, w = im.get_size_out()

        url = getattr(im, '_url', None)
        if url is not None:
            self._svgwriter.write(u'<a xlink:href="%s">' % url)
        self._svgwriter.write(
            u'<image x="%f" y="%f" width="%f" height="%f" '
            u'%s %s xlink:href="' %
            (x / trans[0],
             (self.height - y) / trans[3] - h, w, h, transstr, clippath))

        if rcParams['svg.image_inline']:
            self._svgwriter.write(u"data:image/png;base64,\n")
            bytesio = io.BytesIO()
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, bytesio)
            im.flipud_out()
            self._svgwriter.write(
                encodebytes(bytesio.getvalue()).decode('ascii'))
        else:
            self._imaged[self.basename] = self._imaged.get(self.basename,
                                                           0) + 1
            filename = '%s.image%d.png' % (self.basename,
                                           self._imaged[self.basename])
            verbose.report('Writing image file for inclusion: %s' % filename)
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, filename)
            im.flipud_out()
            self._svgwriter.write(filename)

        self._svgwriter.write(u'"/>\n')
        if url is not None:
            self._svgwriter.write(u'</a>')

    def _adjust_char_id(self, char_id):
        return char_id.replace(u"%20", u"_")

    def draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
        """
        draw the text by converting them to paths using textpath module.

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. If "TeX", use *usetex* mode.


        """
        # this method works for normal text, mathtext and usetex mode.
        # But currently only utilized by draw_tex method.

        glyph_map = self._glyph_map

        text2path = self._text2path
        color = rgb2hex(gc.get_rgb())
        fontsize = prop.get_size_in_points()

        write = self._svgwriter.write

        if ismath == False:
            font = text2path._get_font(prop)
            _glyphs = text2path.get_glyphs_with_font(
                font, s, glyph_map=glyph_map, return_new_glyphs_only=True)
            glyph_info, glyph_map_new, rects = _glyphs

            _flip = Affine2D().scale(1.0, -1.0)

            if glyph_map_new:
                write(u'<defs>\n')
                for char_id, glyph_path in glyph_map_new.iteritems():
                    path = Path(*glyph_path)
                    path_data = self._convert_path(path, _flip, simplify=False)
                    path_element = u'<path id="%s" d="%s"/>\n' % (
                        char_id, ''.join(path_data))
                    write(path_element)
                write(u'</defs>\n')

                glyph_map.update(glyph_map_new)

            svg = []
            clipid = self._get_gc_clip_svg(gc)
            if clipid is not None:
                svg.append(u'<g clip-path="url(#%s)">\n' % clipid)

            svg.append(u'<g style="fill: %s; opacity: %f" transform="' %
                       (color, gc.get_alpha()))
            if angle != 0:
                svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            elif x != 0 or y != 0:
                svg.append(u'translate(%f,%f)' % (x, y))
            svg.append(u'scale(%f)">\n' % (fontsize / text2path.FONT_SCALE))

            for glyph_id, xposition, yposition, scale in glyph_info:
                svg.append(u'<use xlink:href="#%s"' % glyph_id)
                svg.append(u' x="%f" y="%f"' % (xposition, yposition))
                #(currx * (self.FONT_SCALE / fontsize)))
                svg.append(u'/>\n')

            svg.append(u'</g>\n')
            if clipid is not None:
                svg.append(u'</g>\n')
            svg = u''.join(svg)

        else:
            if ismath == "TeX":
                _glyphs = text2path.get_glyphs_tex(prop,
                                                   s,
                                                   glyph_map=glyph_map)
            else:
                _glyphs = text2path.get_glyphs_mathtext(prop,
                                                        s,
                                                        glyph_map=glyph_map)

            glyph_info, glyph_map_new, rects = _glyphs

            # we store the character glyphs w/o flipping. Instead, the
            # coordinate will be flipped when this characters are
            # used.
            if glyph_map_new:
                write(u'<defs>\n')
                for char_id, glyph_path in glyph_map_new.iteritems():
                    char_id = self._adjust_char_id(char_id)
                    path = Path(*glyph_path)
                    path_data = self._convert_path(path, None,
                                                   simplify=False)  #_flip)
                    path_element = u'<path id="%s" d="%s"/>\n' % (
                        char_id, ''.join(path_data))
                    write(path_element)
                write(u'</defs>\n')

                glyph_map.update(glyph_map_new)

            svg = []
            clipid = self._get_gc_clip_svg(gc)
            if clipid is not None:
                svg.append(u'<g clip-path="url(#%s)">\n' % clipid)

            svg.append(u'<g style="fill: %s; opacity: %f" transform="' %
                       (color, gc.get_alpha()))
            if angle != 0:
                svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            elif x != 0 or y != 0:
                svg.append(u'translate(%f,%f)' % (x, y))
            svg.append(u'scale(%f,-%f)">\n' %
                       (fontsize / text2path.FONT_SCALE,
                        fontsize / text2path.FONT_SCALE))

            for char_id, xposition, yposition, scale in glyph_info:
                char_id = self._adjust_char_id(char_id)
                svg.append(u'<use xlink:href="#%s"' % char_id)
                svg.append(u' x="%f" y="%f" transform="scale(%f)"' %
                           (xposition / scale, yposition / scale, scale))
                svg.append(u'/>\n')

            for verts, codes in rects:
                path = Path(verts, codes)
                path_data = self._convert_path(path, None, simplify=False)
                path_element = u'<path d="%s"/>\n' % (''.join(path_data))
                svg.append(path_element)

            svg.append(u'</g><!-- style -->\n')
            if clipid is not None:
                svg.append(u'</g><!-- clipid -->\n')
            svg = u''.join(svg)

        write(svg)

    def draw_tex(self, gc, x, y, s, prop, angle):
        self.draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")

    def draw_text(self, gc, x, y, s, prop, angle, ismath):

        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
            return

        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        y -= font.get_descent() / 64.0

        fontsize = prop.get_size_in_points()
        color = rgb2hex(gc.get_rgb())
        write = self._svgwriter.write

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for c in s:
                path = self._add_char_def(prop, ord(c))
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write(u'<defs>\n')
                for path in new_chars:
                    write(path)
                write(u'</defs>\n')

            svg = []
            clipid = self._get_gc_clip_svg(gc)
            if clipid is not None:
                svg.append(u'<g clip-path="url(#%s)">\n' % clipid)

            svg.append(u'<g style="fill: %s; opacity: %f" transform="' %
                       (color, gc.get_alpha()))
            if angle != 0:
                svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            elif x != 0 or y != 0:
                svg.append(u'translate(%f,%f)' % (x, y))
            svg.append(u'scale(%f)">\n' % (fontsize / self.FONT_SCALE))

            cmap = font.get_charmap()
            lastgind = None
            currx = 0
            for c in s:
                charnum = self._get_char_def_id(prop, ord(c))
                ccode = ord(c)
                gind = cmap.get(ccode)
                if gind is None:
                    ccode = ord('?')
                    gind = 0
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)

                if lastgind is not None:
                    kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
                else:
                    kern = 0
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append(u'<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(u' x="%f"' % (currx *
                                             (self.FONT_SCALE / fontsize)))
                svg.append(u'/>\n')

                currx += (glyph.linearHoriAdvance /
                          65536.0) / (self.FONT_SCALE / fontsize)
                lastgind = gind
            svg.append(u'</g>\n')
            if clipid is not None:
                svg.append(u'</g>\n')
            svg = u''.join(svg)
        else:
            thetext = escape_xml_text(s)
            fontfamily = font.family_name
            fontstyle = prop.get_style()

            style = (
                u'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f'
                % (fontsize, fontfamily, fontstyle, color, gc.get_alpha()))
            if angle != 0:
                transform = u'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (
                    x, y, -angle, -x, -y)
                # Inkscape doesn't support rotate(angle x y)
            else:
                transform = u''

            svg = u"""\
<text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text>
""" % locals()
        write(svg)

    def _add_char_def(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote(u'%s-%d' % (ps_name, char))
        char_num = self._char_defs.get(char_id, None)
        if char_num is not None:
            return None

        path_data = []
        glyph = font.load_char(char, flags=LOAD_NO_HINTING)
        currx, curry = 0.0, 0.0
        for step in glyph.path:
            if step[0] == 0:  # MOVE_TO
                path_data.append(u"M%f %f" % (step[1], -step[2]))
            elif step[0] == 1:  # LINE_TO
                path_data.append(u"l%f %f" %
                                 (step[1] - currx, -step[2] - curry))
            elif step[0] == 2:  # CURVE3
                path_data.append(u"q%f %f %f %f" %
                                 (step[1] - currx, -step[2] - curry,
                                  step[3] - currx, -step[4] - curry))
            elif step[0] == 3:  # CURVE4
                path_data.append(
                    u"c%f %f %f %f %f %f" %
                    (step[1] - currx, -step[2] - curry, step[3] - currx,
                     -step[4] - curry, step[5] - currx, -step[6] - curry))
            elif step[0] == 4:  # ENDPOLY
                path_data.append(u"z")
                currx, curry = 0.0, 0.0

            if step[0] != 4:
                currx, curry = step[-2], -step[-1]
        path_data = u''.join(path_data)
        char_num = u'c_%s' % md5(path_data.encode('ascii')).hexdigest()
        path_element = u'<path id="%s" d="%s"/>\n' % (char_num,
                                                      ''.join(path_data))
        self._char_defs[char_id] = char_num
        return path_element

    def _get_char_def_id(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote(u'%s-%d' % (ps_name, char))
        return self._char_defs[char_id]

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw math text using matplotlib.mathtext
        """
        width, height, descent, svg_elements, used_characters = \
            self.mathtext_parser.parse(s, 72, prop)
        svg_glyphs = svg_elements.svg_glyphs
        svg_rects = svg_elements.svg_rects
        color = rgb2hex(gc.get_rgb())
        write = self._svgwriter.write

        style = u"fill: %s" % color

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for font, fontsize, char, new_x, new_y_mtc, metrics in svg_glyphs:
                path = self._add_char_def(font, char)
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write(u'<defs>\n')
                for path in new_chars:
                    write(path)
                write(u'</defs>\n')

            svg = [u'<g style="%s" transform="' % style]
            if angle != 0:
                svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append(u'translate(%f,%f)' % (x, y))
            svg.append(u'">\n')

            for font, fontsize, char, new_x, new_y_mtc, metrics in svg_glyphs:
                charid = self._get_char_def_id(font, char)

                svg.append(
                    u'<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n'
                    % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE))
            svg.append(u'</g>\n')
        else:  # not rcParams['svg.embed_char_paths']
            svg = [u'<text style="%s" x="%f" y="%f"' % (style, x, y)]

            if angle != 0:
                svg.append(
                    u' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"'
                    % (x, y, -angle, -x,
                       -y))  # Inkscape doesn't support rotate(angle x y)
            svg.append(u'>\n')

            curr_x, curr_y = 0.0, 0.0

            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                new_y = -new_y_mtc
                style = u"font-size: %f; font-family: %s" % (fontsize,
                                                             font.family_name)

                svg.append(u'<tspan style="%s"' % style)
                xadvance = metrics.advance
                svg.append(u' textLength="%f"' % xadvance)

                dx = new_x - curr_x
                if dx != 0.0:
                    svg.append(u' dx="%f"' % dx)

                dy = new_y - curr_y
                if dy != 0.0:
                    svg.append(u' dy="%f"' % dy)

                thetext = escape_xml_text(thetext)

                svg.append(u'>%s</tspan>\n' % thetext)

                curr_x = new_x + xadvance
                curr_y = new_y

            svg.append(u'</text>\n')

        if len(svg_rects):
            style = u"fill: %s; stroke: none" % color
            svg.append(u'<g style="%s" transform="' % style)
            if angle != 0:
                svg.append(u'translate(%f,%f) rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append(u'translate(%f,%f)' % (x, y))
            svg.append(u'">\n')

            for x, y, width, height in svg_rects:
                svg.append(
                    u'<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />'
                    % (x, -y + height, width, height))
            svg.append(u"</g>")

        self.open_group("mathtext")
        write(u''.join(svg))
        self.close_group("mathtext")

    def finalize(self):
        write = self._svgwriter.write
        write(u'</svg>\n')

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if rcParams['text.usetex']:
            size = prop.get_size_in_points()
            texmanager = self._text2path.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s,
                                                               fontsize,
                                                               renderer=self)
            return w, h, d

        if ismath:
            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width, height, descent
        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        w, h = font.get_width_height()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d = font.get_descent()
        d /= 64.0
        return w, h, d
Example #20
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """
    debug = 1
    texd = maxdict(50)  # a cache of tex image rasters
    _fontd = maxdict(50)

    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        if __debug__:
            verbose.report(
                'RendererAgg.__init__ width=%s, height=%s' % (width, height),
                'debug-annoying')
        self._renderer = _RendererAgg(int(width),
                                      int(height),
                                      dpi,
                                      debug=False)
        if __debug__:
            verbose.report('RendererAgg.__init__ _RendererAgg done',
                           'debug-annoying')
        self.draw_path = self._renderer.draw_path
        self.draw_markers = self._renderer.draw_markers
        self.draw_path_collection = self._renderer.draw_path_collection
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.draw_image = self._renderer.draw_image
        self.copy_from_bbox = self._renderer.copy_from_bbox
        self.restore_region = self._renderer.restore_region
        self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized
        self.mathtext_parser = MathTextParser('Agg')

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__:
            verbose.report('RendererAgg.__init__ done', 'debug-annoying')

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        if __debug__:
            verbose.report('RendererAgg.draw_mathtext', 'debug-annoying')
        ox, oy, width, height, descent, font_image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)

        x = int(x) + ox
        y = int(y) - oy
        self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        """
        Render the text
        """
        if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying')

        if ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)

        font = self._get_agg_font(prop)
        if font is None: return None
        if len(s) == 1 and ord(s) > 127:
            font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT)
        else:
            # We pass '0' for angle here, since it will be rotated (in raster
            # space) in the following call to draw_text_image).
            font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT)
        font.draw_glyphs_to_bitmap()

        #print x, y, int(x), int(y)

        self._renderer.draw_text_image(font.get_image(), int(x),
                                       int(y) + 1, angle, gc)

    def get_text_width_height_descent(self, s, prop, ismath):
        """
        get the width and height in display coords of the string s
        with FontPropertry prop

        # passing rgb is a little hack to make cacheing in the
        # texmanager more efficient.  It is not meant to be used
        # outside the backend
        """
        if ismath == 'TeX':
            # todo: handle props
            size = prop.get_size_in_points()
            texmanager = self.get_texmanager()
            Z = texmanager.get_grey(s, size, self.dpi)
            m, n = Z.shape
            # TODO: descent of TeX text (I am imitating backend_ps here -JKS)
            return n, m, 0

        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
        font = self._get_agg_font(prop)
        font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT
                      )  # the width and height of unrotated string
        w, h = font.get_width_height()
        d = font.get_descent()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d /= 64.0
        return w, h, d

    def draw_tex(self, gc, x, y, s, prop, angle):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()

        texmanager = self.get_texmanager()
        key = s, size, self.dpi, angle, texmanager.get_font_config()
        im = self.texd.get(key)
        if im is None:
            Z = texmanager.get_grey(s, size, self.dpi)
            Z = npy.array(Z * 255.0, npy.uint8)

        self._renderer.draw_text_image(Z, x, y, angle, gc)

    def get_canvas_width_height(self):
        'return the canvas width and height in display coords'
        return self.width, self.height

    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, cacheing for efficiency
        """
        if __debug__:
            verbose.report('RendererAgg._get_agg_font', 'debug-annoying')

        key = hash(prop)
        font = self._fontd.get(key)

        if font is None:
            fname = findfont(prop)
            font = self._fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self._fontd[fname] = font
            self._fontd[key] = font

        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, self.dpi)

        return font

    def points_to_pixels(self, points):
        """
        convert point measures to pixes using dpi and the pixels per
        inch of the display
        """
        if __debug__:
            verbose.report('RendererAgg.points_to_pixels', 'debug-annoying')
        return points * self.dpi / 72.0

    def tostring_rgb(self):
        if __debug__:
            verbose.report('RendererAgg.tostring_rgb', 'debug-annoying')
        return self._renderer.tostring_rgb()

    def tostring_argb(self):
        if __debug__:
            verbose.report('RendererAgg.tostring_argb', 'debug-annoying')
        return self._renderer.tostring_argb()

    def buffer_rgba(self, x, y):
        if __debug__:
            verbose.report('RendererAgg.buffer_rgba', 'debug-annoying')
        return self._renderer.buffer_rgba(x, y)

    def clear(self):
        self._renderer.clear()
Example #21
0
class RendererMac(RendererBase):
    """
    The renderer handles drawing/rendering operations. Most of the renderer's
    methods forwards the command to the renderer's graphics context. The
    renderer does not wrap a C object and is written in pure Python.
    """

    texd = maxdict(50)  # a cache of tex image rasters

    def __init__(self, dpi, width, height):
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        self.gc = GraphicsContextMac()
        self.mathtext_parser = MathTextParser('MacOSX')

    def set_width_height(self, width, height):
        self.width, self.height = width, height

    def draw_path(self, gc, path, transform, rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        if gc != self.gc:
            n = self.gc.level() - gc.level()
            for i in range(n):
                self.gc.restore()
            self.gc = gc
        gc.draw_path(path, transform, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        if gc != self.gc:
            n = self.gc.level() - gc.level()
            for i in range(n):
                self.gc.restore()
            self.gc = gc
        gc.draw_markers(marker_path, marker_trans, path, trans, rgbFace)

    def draw_path_collection(self, *args):
        gc = self.gc
        args = args[:13]
        gc.draw_path_collection(*args)

    def draw_quad_mesh(self, *args):
        gc = self.gc
        gc.draw_quad_mesh(*args)

    def new_gc(self):
        self.gc.reset()
        return self.gc

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        im.flipud_out()
        nrows, ncols, data = im.as_rgba_str()
        self.gc.draw_image(x, y, nrows, ncols, data, bbox, clippath,
                           clippath_trans)
        im.flipud_out()

    def draw_tex(self, gc, x, y, s, prop, angle):
        if gc != self.gc:
            n = self.gc.level() - gc.level()
            for i in range(n):
                self.gc.restore()
            self.gc = gc
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()
        texmanager = self.get_texmanager()
        key = s, size, self.dpi, angle, texmanager.get_font_config()
        im = self.texd.get(
            key)  # Not sure what this does; just copied from backend_agg.py
        if im is None:
            Z = texmanager.get_grey(s, size, self.dpi)
            Z = numpy.array(255.0 - Z * 255.0, numpy.uint8)

        gc.draw_mathtext(x, y, angle, Z)

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if gc != self.gc:
            n = self.gc.level() - gc.level()
            for i in range(n):
                self.gc.restore()
            self.gc = gc
        size = prop.get_size_in_points()
        ox, oy, width, height, descent, image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)
        gc.draw_mathtext(x, y, angle, 255 - image.as_array())

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
        if gc != self.gc:
            n = self.gc.level() - gc.level()
            for i in range(n):
                self.gc.restore()
            self.gc = gc
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
            family = prop.get_family()
            size = prop.get_size_in_points()
            weight = prop.get_weight()
            style = prop.get_style()
            gc.draw_text(x, y, unicode(s), family, size, weight, style, angle)

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath == 'TeX':
            # TODO: handle props
            size = prop.get_size_in_points()
            texmanager = self.get_texmanager()
            Z = texmanager.get_grey(s, size, self.dpi)
            m, n = Z.shape
            # TODO: handle descent; This is based on backend_agg.py
            return n, m, 0
        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
        family = prop.get_family()
        size = prop.get_size_in_points()
        weight = prop.get_weight()
        style = prop.get_style()
        return self.gc.get_text_width_height_descent(unicode(s), family, size,
                                                     weight, style)

    def flipy(self):
        return False

    def points_to_pixels(self, points):
        return points / 72.0 * self.dpi

    def option_image_nocomposite(self):
        return True
Example #22
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

    def __init__(self, width, height, svgwriter, basename=None):
        self.width = width
        self.height = height
        self._svgwriter = svgwriter

        self._groupd = {}
        if not rcParams['svg.image_inline']:
            assert basename is not None
            self.basename = basename
            self._imaged = {}
        self._clipd = {}
        self._char_defs = {}
        self._markers = {}
        self._path_collection_id = 0
        self._imaged = {}
        self.mathtext_parser = MathTextParser('SVG')
        svgwriter.write(svgProlog % (width, height, width, height))

    def _draw_svg_element(self, element, details, gc, rgbFace):
        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        style = self._get_style(gc, rgbFace)
        self._svgwriter.write('<%s style="%s" %s %s/>\n' %
                              (element, style, clippath, details))

    def _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _get_style(self, gc, rgbFace):
        """
        return the style string.
        style is generated from the GraphicsContext, rgbFace and clippath
        """
        if rgbFace is None:
            fill = 'none'
        else:
            fill = rgb2hex(rgbFace[:3])

        offset, seq = gc.get_dashes()
        if seq is None:
            dashes = ''
        else:
            dashes = 'stroke-dasharray: %s; stroke-dashoffset: %s;' % (
                ','.join(['%s' % val for val in seq]), offset)

        linewidth = gc.get_linewidth()
        if linewidth:
            return 'fill: %s; stroke: %s; stroke-width: %s; ' \
                'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %s' % (
                         fill,
                         rgb2hex(gc.get_rgb()[:3]),
                         linewidth,
                         gc.get_joinstyle(),
                         _capstyle_d[gc.get_capstyle()],
                         dashes,
                         gc.get_alpha(),
                )
        else:
            return 'fill: %s; opacity: %s' % (\
                         fill,
                         gc.get_alpha(),
                )

    def _get_gc_clip_svg(self, gc):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_trans = gc.get_clip_path()
        if clippath is not None:
            path_data = self._convert_path(clippath, clippath_trans)
            path = '<path d="%s"/>' % path_data
        elif cliprect is not None:
            x, y, w, h = cliprect.bounds
            y = self.height - (y + h)
            path = '<rect x="%(x)s" y="%(y)s" width="%(w)s" height="%(h)s"/>' % locals(
            )
        else:
            return None

        id = self._clipd.get(path)
        if id is None:
            id = 'p%s' % md5(path).hexdigest()
            self._svgwriter.write('<defs>\n  <clipPath id="%s">\n' % id)
            self._svgwriter.write(path)
            self._svgwriter.write('\n  </clipPath>\n</defs>')
            self._clipd[path] = id
        return id

    def open_group(self, s):
        self._groupd[s] = self._groupd.get(s, 0) + 1
        self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s]))

    def close_group(self, s):
        self._svgwriter.write('</g>\n')

    def option_image_nocomposite(self):
        """
        if svg.image_noscale is True, compositing multiple images into one is prohibited
        """
        return rcParams['svg.image_noscale']

    _path_commands = {
        Path.MOVETO: 'M%s %s',
        Path.LINETO: 'L%s %s',
        Path.CURVE3: 'Q%s %s %s %s',
        Path.CURVE4: 'C%s %s %s %s %s %s'
    }

    def _make_flip_transform(self, transform):
        return (transform +
                Affine2D().scale(1.0, -1.0).translate(0.0, self.height))

    def _convert_path(self, path, transform):
        tpath = transform.transform_path(path)

        path_data = []
        appender = path_data.append
        path_commands = self._path_commands
        currpos = 0
        for points, code in tpath.iter_segments():
            if code == Path.CLOSEPOLY:
                segment = 'z'
            else:
                segment = path_commands[code] % tuple(points)

            if currpos + len(segment) > 75:
                appender("\n")
                currpos = 0
            appender(segment)
            currpos += len(segment)
        return ''.join(path_data)

    def draw_path(self, gc, path, transform, rgbFace=None):
        trans_and_flip = self._make_flip_transform(transform)
        path_data = self._convert_path(path, trans_and_flip)
        self._draw_svg_element('path', 'd="%s"' % path_data, gc, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        write = self._svgwriter.write

        key = self._convert_path(marker_path,
                                 marker_trans + Affine2D().scale(1.0, -1.0))
        name = self._markers.get(key)
        if name is None:
            name = 'm%s' % md5(key).hexdigest()
            write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key))
            self._markers[key] = name

        clipid = self._get_gc_clip_svg(gc)
        if clipid is None:
            clippath = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        write('<g %s>' % clippath)
        trans_and_flip = self._make_flip_transform(trans)
        tpath = trans_and_flip.transform_path(path)
        for x, y in tpath.vertices:
            details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y)
            style = self._get_style(gc, rgbFace)
            self._svgwriter.write('<use style="%s" %s/>\n' % (style, details))
        write('</g>')

    def draw_path_collection(self, master_transform, cliprect, clippath,
                             clippath_trans, paths, all_transforms, offsets,
                             offsetTrans, facecolors, edgecolors, linewidths,
                             linestyles, antialiaseds):
        write = self._svgwriter.write

        path_codes = []
        write('<defs>\n')
        for i, (path, transform) in enumerate(
                self._iter_collection_raw_paths(master_transform, paths,
                                                all_transforms)):
            transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
            d = self._convert_path(path, transform)
            name = 'coll%x_%x_%s' % (self._path_collection_id, i,
                                     md5(d).hexdigest())
            write('<path id="%s" d="%s"/>\n' % (name, d))
            path_codes.append(name)
        write('</defs>\n')

        for xo, yo, path_id, gc, rgbFace in self._iter_collection(
                path_codes, cliprect, clippath, clippath_trans, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds):
            clipid = self._get_gc_clip_svg(gc)
            if clipid is not None:
                write('<g clip-path="url(#%s)">' % clipid)
            details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo,
                                                          self.height - yo)
            style = self._get_style(gc, rgbFace)
            self._svgwriter.write('<use style="%s" %s/>\n' % (style, details))
            if clipid is not None:
                write('</g>')

        self._path_collection_id += 1

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        # MGDTODO: Support clippath here
        trans = [1, 0, 0, 1, 0, 0]
        transstr = ''
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            if im.get_interpolation() != 0:
                trans[4] += trans[0]
                trans[5] += trans[3]
            trans[5] = -trans[5]
            transstr = 'transform="matrix(%s %s %s %s %s %s)" ' % tuple(trans)
            assert trans[1] == 0
            assert trans[2] == 0
            numrows, numcols = im.get_size()
            im.reset_matrix()
            im.set_interpolation(0)
            im.resize(numcols, numrows)

        h, w = im.get_size_out()

        self._svgwriter.write(
            '<image x="%s" y="%s" width="%s" height="%s" '
            '%s xlink:href="' %
            (x / trans[0], (self.height - y) / trans[3] - h, w, h, transstr))

        if rcParams['svg.image_inline']:
            self._svgwriter.write("data:image/png;base64,\n")
            stringio = cStringIO.StringIO()
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, stringio)
            im.flipud_out()
            self._svgwriter.write(base64.encodestring(stringio.getvalue()))
        else:
            self._imaged[self.basename] = self._imaged.get(self.basename,
                                                           0) + 1
            filename = '%s.image%d.png' % (self.basename,
                                           self._imaged[self.basename])
            verbose.report('Writing image file for inclusion: %s' % filename)
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, filename)
            im.flipud_out()
            self._svgwriter.write(filename)

        self._svgwriter.write('"/>\n')

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
            return

        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        y -= font.get_descent() / 64.0

        fontsize = prop.get_size_in_points()
        color = rgb2hex(gc.get_rgb()[:3])
        write = self._svgwriter.write

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for c in s:
                path = self._add_char_def(prop, c)
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write('<defs>\n')
                for path in new_chars:
                    write(path)
                write('</defs>\n')

            svg = []
            clipid = self._get_gc_clip_svg(gc)
            if clipid is not None:
                svg.append('<g clip-path="url(#%s)">\n' % clipid)

            svg.append('<g style="fill: %s; opacity: %s" transform="' %
                       (color, gc.get_alpha()))
            if angle != 0:
                svg.append('translate(%s,%s)rotate(%1.1f)' % (x, y, -angle))
            elif x != 0 or y != 0:
                svg.append('translate(%s,%s)' % (x, y))
            svg.append('scale(%s)">\n' % (fontsize / self.FONT_SCALE))

            cmap = font.get_charmap()
            lastgind = None
            currx = 0
            for c in s:
                charnum = self._get_char_def_id(prop, c)
                ccode = ord(c)
                gind = cmap.get(ccode)
                if gind is None:
                    ccode = ord('?')
                    gind = 0
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)

                if lastgind is not None:
                    kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
                else:
                    kern = 0
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' x="%s"' % (currx *
                                            (self.FONT_SCALE / fontsize)))
                svg.append('/>\n')

                currx += (glyph.linearHoriAdvance /
                          65536.0) / (self.FONT_SCALE / fontsize)
                lastgind = gind
            svg.append('</g>\n')
            if clipid is not None:
                svg.append('</g>\n')
            svg = ''.join(svg)
        else:
            thetext = escape_xml_text(s)
            fontfamily = font.family_name
            fontstyle = prop.get_style()

            style = (
                'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %s'
                % (fontsize, fontfamily, fontstyle, color, gc.get_alpha()))
            if angle != 0:
                transform = 'transform="translate(%s,%s) rotate(%1.1f) translate(%s,%s)"' % (
                    x, y, -angle, -x, -y)
                # Inkscape doesn't support rotate(angle x y)
            else:
                transform = ''

            svg = """\
<text style="%(style)s" x="%(x)s" y="%(y)s" %(transform)s>%(thetext)s</text>
""" % locals()
        write(svg)

    def _add_char_def(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ord(char)))
        char_num = self._char_defs.get(char_id, None)
        if char_num is not None:
            return None

        path_data = []
        glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING)
        currx, curry = 0.0, 0.0
        for step in glyph.path:
            if step[0] == 0:  # MOVE_TO
                path_data.append("M%s %s" % (step[1], -step[2]))
            elif step[0] == 1:  # LINE_TO
                path_data.append("l%s %s" %
                                 (step[1] - currx, -step[2] - curry))
            elif step[0] == 2:  # CURVE3
                path_data.append("q%s %s %s %s" %
                                 (step[1] - currx, -step[2] - curry,
                                  step[3] - currx, -step[4] - curry))
            elif step[0] == 3:  # CURVE4
                path_data.append(
                    "c%s %s %s %s %s %s" %
                    (step[1] - currx, -step[2] - curry, step[3] - currx,
                     -step[4] - curry, step[5] - currx, -step[6] - curry))
            elif step[0] == 4:  # ENDPOLY
                path_data.append("z")
                currx, curry = 0.0, 0.0

            if step[0] != 4:
                currx, curry = step[-2], -step[-1]
        path_data = ''.join(path_data)
        char_num = 'c_%s' % md5(path_data).hexdigest()
        path_element = '<path id="%s" d="%s"/>\n' % (char_num,
                                                     ''.join(path_data))
        self._char_defs[char_id] = char_num
        return path_element

    def _get_char_def_id(self, prop, char):
        if isinstance(prop, FontProperties):
            newprop = prop.copy()
            font = self._get_font(newprop)
        else:
            font = prop
        font.set_size(self.FONT_SCALE, 72)
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ord(char)))
        return self._char_defs[char_id]

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw math text using matplotlib.mathtext
        """
        width, height, descent, svg_elements, used_characters = \
            self.mathtext_parser.parse(s, 72, prop)
        svg_glyphs = svg_elements.svg_glyphs
        svg_rects = svg_elements.svg_rects
        color = rgb2hex(gc.get_rgb()[:3])
        write = self._svgwriter.write

        style = "fill: %s" % color

        if rcParams['svg.embed_char_paths']:
            new_chars = []
            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                path = self._add_char_def(font, thetext)
                if path is not None:
                    new_chars.append(path)
            if len(new_chars):
                write('<defs>\n')
                for path in new_chars:
                    write(path)
                write('</defs>\n')

            svg = ['<g style="%s" transform="' % style]
            if angle != 0:
                svg.append('translate(%s,%s)rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append('translate(%s,%s)' % (x, y))
            svg.append('">\n')

            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                charid = self._get_char_def_id(font, thetext)

                svg.append(
                    '<use xlink:href="#%s" transform="translate(%s,%s)scale(%s)"/>\n'
                    % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE))
            svg.append('</g>\n')
        else:  # not rcParams['svg.embed_char_paths']
            svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)]

            if angle != 0:
                svg.append(
                    ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"'
                    % (x, y, -angle, -x,
                       -y))  # Inkscape doesn't support rotate(angle x y)
            svg.append('>\n')

            curr_x, curr_y = 0.0, 0.0

            for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs:
                new_y = -new_y_mtc
                style = "font-size: %f; font-family: %s" % (fontsize,
                                                            font.family_name)

                svg.append('<tspan style="%s"' % style)
                xadvance = metrics.advance
                svg.append(' textLength="%s"' % xadvance)

                dx = new_x - curr_x
                if dx != 0.0:
                    svg.append(' dx="%s"' % dx)

                dy = new_y - curr_y
                if dy != 0.0:
                    svg.append(' dy="%s"' % dy)

                thetext = escape_xml_text(thetext)

                svg.append('>%s</tspan>\n' % thetext)

                curr_x = new_x + xadvance
                curr_y = new_y

            svg.append('</text>\n')

        if len(svg_rects):
            style = "fill: %s; stroke: none" % color
            svg.append('<g style="%s" transform="' % style)
            if angle != 0:
                svg.append('translate(%s,%s) rotate(%1.1f)' % (x, y, -angle))
            else:
                svg.append('translate(%s,%s)' % (x, y))
            svg.append('">\n')

            for x, y, width, height in svg_rects:
                svg.append(
                    '<rect x="%s" y="%s" width="%s" height="%s" fill="black" stroke="none" />'
                    % (x, -y + height, width, height))
            svg.append("</g>")

        self.open_group("mathtext")
        write(''.join(svg))
        self.close_group("mathtext")

    def finalize(self):
        write = self._svgwriter.write
        write('</svg>\n')

    def flipy(self):
        return True

    def get_canvas_width_height(self):
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath:
            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width, height, descent
        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        w, h = font.get_width_height()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d = font.get_descent()
        d /= 64.0
        return w, h, d
Example #23
0
class Path(object):
    """
    :class:`Path` represents a series of possibly disconnected,
    possibly closed, line and curve segments.

    The underlying storage is made up of two parallel numpy arrays:
      - *vertices*: an Nx2 float array of vertices
      - *codes*: an N-length uint8 array of vertex types

    These two arrays always have the same length in the first
    dimension.  For example, to represent a cubic curve, you must
    provide three vertices as well as three codes ``CURVE3``.

    The code types are:

       - ``STOP``   :  1 vertex (ignored)
           A marker for the end of the entire path (currently not
           required and ignored)

       - ``MOVETO`` :  1 vertex
            Pick up the pen and move to the given vertex.

       - ``LINETO`` :  1 vertex
            Draw a line from the current position to the given vertex.

       - ``CURVE3`` :  1 control point, 1 endpoint
          Draw a quadratic Bezier curve from the current position,
          with the given control point, to the given end point.

       - ``CURVE4`` :  2 control points, 1 endpoint
          Draw a cubic Bezier curve from the current position, with
          the given control points, to the given end point.

       - ``CLOSEPOLY`` : 1 vertex (ignored)
          Draw a line segment to the start point of the current
          polyline.

    Users of Path objects should not access the vertices and codes
    arrays directly.  Instead, they should use :meth:`iter_segments`
    or :meth:`cleaned` to get the vertex/code pairs.  This is important,
    since many :class:`Path` objects, as an optimization, do not store a
    *codes* at all, but have a default one provided for them by
    :meth:`iter_segments`.

    .. note::

        The vertices and codes arrays should be treated as
        immutable -- there are a number of optimizations and assumptions
        made up front in the constructor that will not change when the
        data changes.

    """

    # Path codes
    STOP = 0  # 1 vertex
    MOVETO = 1  # 1 vertex
    LINETO = 2  # 1 vertex
    CURVE3 = 3  # 2 vertices
    CURVE4 = 4  # 3 vertices
    CLOSEPOLY = 79  # 1 vertex

    #: A dictionary mapping Path codes to the number of vertices that the
    #: code expects.
    NUM_VERTICES_FOR_CODE = {
        STOP: 1,
        MOVETO: 1,
        LINETO: 1,
        CURVE3: 2,
        CURVE4: 3,
        CLOSEPOLY: 1
    }

    code_type = np.uint8

    def __init__(self,
                 vertices,
                 codes=None,
                 _interpolation_steps=1,
                 closed=False,
                 readonly=False):
        """
        Create a new path with the given vertices and codes.

        Parameters
        ----------
        vertices : array_like
            The ``(n, 2)`` float array, masked array or sequence of pairs
            representing the vertices of the path.

            If *vertices* contains masked values, they will be converted
            to NaNs which are then handled correctly by the Agg
            PathIterator and other consumers of path data, such as
            :meth:`iter_segments`.
        codes : {None, array_like}, optional
            n-length array integers representing the codes of the path.
            If not None, codes must be the same length as vertices.
            If None, *vertices* will be treated as a series of line segments.
        _interpolation_steps : int, optional
            Used as a hint to certain projections, such as Polar, that this
            path should be linearly interpolated immediately before drawing.
            This attribute is primarily an implementation detail and is not
            intended for public use.
        closed : bool, optional
            If *codes* is None and closed is True, vertices will be treated as
            line segments of a closed polygon.
        readonly : bool, optional
            Makes the path behave in an immutable way and sets the vertices
            and codes as read-only arrays.
        """
        if isinstance(vertices, np.ma.MaskedArray):
            vertices = vertices.astype(float).filled(np.nan)
        else:
            vertices = np.asarray(vertices, float)

        if (vertices.ndim != 2) or (vertices.shape[1] != 2):
            msg = "'vertices' must be a 2D list or array with shape Nx2"
            raise ValueError(msg)

        if codes is not None:
            codes = np.asarray(codes, self.code_type)
            if (codes.ndim != 1) or len(codes) != len(vertices):
                msg = ("'codes' must be a 1D list or array with the same"
                       " length of 'vertices'")
                raise ValueError(msg)
            if len(codes) and codes[0] != self.MOVETO:
                msg = ("The first element of 'code' must be equal to 'MOVETO':"
                       " {0}")
                raise ValueError(msg.format(self.MOVETO))
        elif closed:
            codes = np.empty(len(vertices), dtype=self.code_type)
            codes[0] = self.MOVETO
            codes[1:-1] = self.LINETO
            codes[-1] = self.CLOSEPOLY

        self._vertices = vertices
        self._codes = codes
        self._interpolation_steps = _interpolation_steps
        self._update_values()

        if readonly:
            self._vertices.flags.writeable = False
            if self._codes is not None:
                self._codes.flags.writeable = False
            self._readonly = True
        else:
            self._readonly = False

    @classmethod
    def _fast_from_codes_and_verts(cls, verts, codes, internals=None):
        """
        Creates a Path instance without the expense of calling the constructor

        Parameters
        ----------
        verts : numpy array
        codes : numpy array
        internals : dict or None
            The attributes that the resulting path should have.
            Allowed keys are ``readonly``, ``should_simplify``,
            ``simplify_threshold``, ``has_nonfinite`` and
            ``interpolation_steps``.

        """
        internals = internals or {}
        pth = cls.__new__(cls)
        if isinstance(verts, np.ma.MaskedArray):
            verts = verts.astype(float).filled(np.nan)
        else:
            verts = np.asarray(verts, float)
        pth._vertices = verts
        pth._codes = codes
        pth._readonly = internals.pop('readonly', False)
        pth.should_simplify = internals.pop('should_simplify', True)
        pth.simplify_threshold = (internals.pop(
            'simplify_threshold', rcParams['path.simplify_threshold']))
        pth._has_nonfinite = internals.pop('has_nonfinite', False)
        pth._interpolation_steps = internals.pop('interpolation_steps', 1)
        if internals:
            raise ValueError('Unexpected internals provided to '
                             '_fast_from_codes_and_verts: '
                             '{0}'.format('\n *'.join(
                                 six.iterkeys(internals))))
        return pth

    def _update_values(self):
        self._should_simplify = (
            rcParams['path.simplify']
            and (len(self._vertices) >= 128 and
                 (self._codes is None or np.all(self._codes <= Path.LINETO))))
        self._simplify_threshold = rcParams['path.simplify_threshold']
        self._has_nonfinite = not np.isfinite(self._vertices).all()

    @property
    def vertices(self):
        """
        The list of vertices in the `Path` as an Nx2 numpy array.
        """
        return self._vertices

    @vertices.setter
    def vertices(self, vertices):
        if self._readonly:
            raise AttributeError("Can't set vertices on a readonly Path")
        self._vertices = vertices
        self._update_values()

    @property
    def codes(self):
        """
        The list of codes in the `Path` as a 1-D numpy array.  Each
        code is one of `STOP`, `MOVETO`, `LINETO`, `CURVE3`, `CURVE4`
        or `CLOSEPOLY`.  For codes that correspond to more than one
        vertex (`CURVE3` and `CURVE4`), that code will be repeated so
        that the length of `self.vertices` and `self.codes` is always
        the same.
        """
        return self._codes

    @codes.setter
    def codes(self, codes):
        if self._readonly:
            raise AttributeError("Can't set codes on a readonly Path")
        self._codes = codes
        self._update_values()

    @property
    def simplify_threshold(self):
        """
        The fraction of a pixel difference below which vertices will
        be simplified out.
        """
        return self._simplify_threshold

    @simplify_threshold.setter
    def simplify_threshold(self, threshold):
        self._simplify_threshold = threshold

    @property
    def has_nonfinite(self):
        """
        `True` if the vertices array has nonfinite values.
        """
        return self._has_nonfinite

    @property
    def should_simplify(self):
        """
        `True` if the vertices array should be simplified.
        """
        return self._should_simplify

    @should_simplify.setter
    def should_simplify(self, should_simplify):
        self._should_simplify = should_simplify

    @property
    def readonly(self):
        """
        `True` if the `Path` is read-only.
        """
        return self._readonly

    def __copy__(self):
        """
        Returns a shallow copy of the `Path`, which will share the
        vertices and codes with the source `Path`.
        """
        import copy
        return copy.copy(self)

    copy = __copy__

    def __deepcopy__(self, memo=None):
        """
        Returns a deepcopy of the `Path`.  The `Path` will not be
        readonly, even if the source `Path` is.
        """
        try:
            codes = self.codes.copy()
        except AttributeError:
            codes = None
        return self.__class__(self.vertices.copy(),
                              codes,
                              _interpolation_steps=self._interpolation_steps)

    deepcopy = __deepcopy__

    @classmethod
    def make_compound_path_from_polys(cls, XY):
        """
        Make a compound path object to draw a number
        of polygons with equal numbers of sides XY is a (numpolys x
        numsides x 2) numpy array of vertices.  Return object is a
        :class:`Path`

        .. plot:: mpl_examples/api/histogram_path_demo.py

        """

        # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for
        # the CLOSEPOLY; the vert for the closepoly is ignored but we still
        # need it to keep the codes aligned with the vertices
        numpolys, numsides, two = XY.shape
        if two != 2:
            raise ValueError("The third dimension of 'XY' must be 2")
        stride = numsides + 1
        nverts = numpolys * stride
        verts = np.zeros((nverts, 2))
        codes = np.ones(nverts, int) * cls.LINETO
        codes[0::stride] = cls.MOVETO
        codes[numsides::stride] = cls.CLOSEPOLY
        for i in range(numsides):
            verts[i::stride] = XY[:, i]

        return cls(verts, codes)

    @classmethod
    def make_compound_path(cls, *args):
        """Make a compound path from a list of Path objects."""
        # Handle an empty list in args (i.e. no args).
        if not args:
            return Path(np.empty([0, 2], dtype=np.float32))

        lengths = [len(x) for x in args]
        total_length = sum(lengths)

        vertices = np.vstack([x.vertices for x in args])
        vertices.reshape((total_length, 2))

        codes = np.empty(total_length, dtype=cls.code_type)
        i = 0
        for path in args:
            if path.codes is None:
                codes[i] = cls.MOVETO
                codes[i + 1:i + len(path.vertices)] = cls.LINETO
            else:
                codes[i:i + len(path.codes)] = path.codes
            i += len(path.vertices)

        return cls(vertices, codes)

    def __repr__(self):
        return "Path(%r, %r)" % (self.vertices, self.codes)

    def __len__(self):
        return len(self.vertices)

    def iter_segments(self,
                      transform=None,
                      remove_nans=True,
                      clip=None,
                      snap=False,
                      stroke_width=1.0,
                      simplify=None,
                      curves=True,
                      sketch=None):
        """
        Iterates over all of the curve segments in the path.  Each
        iteration returns a 2-tuple (*vertices*, *code*), where
        *vertices* is a sequence of 1 - 3 coordinate pairs, and *code* is
        one of the :class:`Path` codes.

        Additionally, this method can provide a number of standard
        cleanups and conversions to the path.

        Parameters
        ----------
        transform : None or :class:`~matplotlib.transforms.Transform` instance
            If not None, the given affine transformation will
            be applied to the path.
        remove_nans : {False, True}, optional
            If True, will remove all NaNs from the path and
            insert MOVETO commands to skip over them.
        clip : None or sequence, optional
            If not None, must be a four-tuple (x1, y1, x2, y2)
            defining a rectangle in which to clip the path.
        snap : None or bool, optional
            If None, auto-snap to pixels, to reduce
            fuzziness of rectilinear lines.  If True, force snapping, and
            if False, don't snap.
        stroke_width : float, optional
            The width of the stroke being drawn.  Needed
             as a hint for the snapping algorithm.
        simplify : None or bool, optional
            If True, perform simplification, to remove
             vertices that do not affect the appearance of the path.  If
             False, perform no simplification.  If None, use the
             should_simplify member variable.
        curves : {True, False}, optional
            If True, curve segments will be returned as curve
            segments.  If False, all curves will be converted to line
            segments.
        sketch : None or sequence, optional
            If not None, must be a 3-tuple of the form
            (scale, length, randomness), representing the sketch
            parameters.
        """
        if not len(self):
            return

        cleaned = self.cleaned(transform=transform,
                               remove_nans=remove_nans,
                               clip=clip,
                               snap=snap,
                               stroke_width=stroke_width,
                               simplify=simplify,
                               curves=curves,
                               sketch=sketch)
        vertices = cleaned.vertices
        codes = cleaned.codes
        len_vertices = vertices.shape[0]

        # Cache these object lookups for performance in the loop.
        NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE
        STOP = self.STOP

        i = 0
        while i < len_vertices:
            code = codes[i]
            if code == STOP:
                return
            else:
                num_vertices = NUM_VERTICES_FOR_CODE[code]
                curr_vertices = vertices[i:i + num_vertices].flatten()
                yield curr_vertices, code
                i += num_vertices

    def cleaned(self,
                transform=None,
                remove_nans=False,
                clip=None,
                quantize=False,
                simplify=False,
                curves=False,
                stroke_width=1.0,
                snap=False,
                sketch=None):
        """
        Cleans up the path according to the parameters returning a new
        Path instance.

        .. seealso::

            See :meth:`iter_segments` for details of the keyword arguments.

        Returns
        -------
        Path instance with cleaned up vertices and codes.

        """
        vertices, codes = _path.cleanup_path(self, transform, remove_nans,
                                             clip, snap, stroke_width,
                                             simplify, curves, sketch)
        internals = {
            'should_simplify': self.should_simplify and not simplify,
            'has_nonfinite': self.has_nonfinite and not remove_nans,
            'simplify_threshold': self.simplify_threshold,
            'interpolation_steps': self._interpolation_steps
        }
        return Path._fast_from_codes_and_verts(vertices, codes, internals)

    def transformed(self, transform):
        """
        Return a transformed copy of the path.

        .. seealso::

            :class:`matplotlib.transforms.TransformedPath`
                A specialized path class that will cache the
                transformed result and automatically update when the
                transform changes.
        """
        return Path(transform.transform(self.vertices), self.codes,
                    self._interpolation_steps)

    def contains_point(self, point, transform=None, radius=0.0):
        """
        Returns *True* if the path contains the given point.

        If *transform* is not *None*, the path will be transformed
        before performing the test.

        *radius* allows the path to be made slightly larger or
        smaller.
        """
        if transform is not None:
            transform = transform.frozen()
        result = _path.point_in_path(point[0], point[1], radius, self,
                                     transform)
        return result

    def contains_points(self, points, transform=None, radius=0.0):
        """
        Returns a bool array which is *True* if the path contains the
        corresponding point.

        If *transform* is not *None*, the path will be transformed
        before performing the test.

        *radius* allows the path to be made slightly larger or
        smaller.
        """
        if transform is not None:
            transform = transform.frozen()
        result = _path.points_in_path(points, radius, self, transform)
        return result.astype('bool')

    def contains_path(self, path, transform=None):
        """
        Returns *True* if this path completely contains the given path.

        If *transform* is not *None*, the path will be transformed
        before performing the test.
        """
        if transform is not None:
            transform = transform.frozen()
        return _path.path_in_path(self, None, path, transform)

    def get_extents(self, transform=None):
        """
        Returns the extents (*xmin*, *ymin*, *xmax*, *ymax*) of the
        path.

        Unlike computing the extents on the *vertices* alone, this
        algorithm will take into account the curves and deal with
        control points appropriately.
        """
        from .transforms import Bbox
        path = self
        if transform is not None:
            transform = transform.frozen()
            if not transform.is_affine:
                path = self.transformed(transform)
                transform = None
        return Bbox(_path.get_path_extents(path, transform))

    def intersects_path(self, other, filled=True):
        """
        Returns *True* if this path intersects another given path.

        *filled*, when True, treats the paths as if they were filled.
        That is, if one path completely encloses the other,
        :meth:`intersects_path` will return True.
        """
        return _path.path_intersects_path(self, other, filled)

    def intersects_bbox(self, bbox, filled=True):
        """
        Returns *True* if this path intersects a given
        :class:`~matplotlib.transforms.Bbox`.

        *filled*, when True, treats the path as if it was filled.
        That is, if one path completely encloses the other,
        :meth:`intersects_path` will return True.
        """
        from .transforms import BboxTransformTo
        rectangle = self.unit_rectangle().transformed(BboxTransformTo(bbox))
        result = self.intersects_path(rectangle, filled)
        return result

    def interpolated(self, steps):
        """
        Returns a new path resampled to length N x steps.  Does not
        currently handle interpolating curves.
        """
        if steps == 1:
            return self

        vertices = simple_linear_interpolation(self.vertices, steps)
        codes = self.codes
        if codes is not None:
            new_codes = Path.LINETO * np.ones(((len(codes) - 1) * steps + 1, ))
            new_codes[0::steps] = codes
        else:
            new_codes = None
        return Path(vertices, new_codes)

    def to_polygons(self, transform=None, width=0, height=0, closed_only=True):
        """
        Convert this path to a list of polygons or polylines.  Each
        polygon/polyline is an Nx2 array of vertices.  In other words,
        each polygon has no ``MOVETO`` instructions or curves.  This
        is useful for displaying in backends that do not support
        compound paths or Bezier curves, such as GDK.

        If *width* and *height* are both non-zero then the lines will
        be simplified so that vertices outside of (0, 0), (width,
        height) will be clipped.

        If *closed_only* is `True` (default), only closed polygons,
        with the last point being the same as the first point, will be
        returned.  Any unclosed polylines in the path will be
        explicitly closed.  If *closed_only* is `False`, any unclosed
        polygons in the path will be returned as unclosed polygons,
        and the closed polygons will be returned explicitly closed by
        setting the last point to the same as the first point.
        """
        if len(self.vertices) == 0:
            return []

        if transform is not None:
            transform = transform.frozen()

        if self.codes is None and (width == 0 or height == 0):
            vertices = self.vertices
            if closed_only:
                if len(vertices) < 3:
                    return []
                elif np.any(vertices[0] != vertices[-1]):
                    vertices = list(vertices) + [vertices[0]]

            if transform is None:
                return [vertices]
            else:
                return [transform.transform(vertices)]

        # Deal with the case where there are curves and/or multiple
        # subpaths (using extension code)
        return _path.convert_path_to_polygons(self, transform, width, height,
                                              closed_only)

    _unit_rectangle = None

    @classmethod
    def unit_rectangle(cls):
        """
        Return a :class:`Path` instance of the unit rectangle
        from (0, 0) to (1, 1).
        """
        if cls._unit_rectangle is None:
            cls._unit_rectangle = \
                cls([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],
                     [0.0, 0.0]],
                    [cls.MOVETO, cls.LINETO, cls.LINETO, cls.LINETO,
                     cls.CLOSEPOLY],
                    readonly=True)
        return cls._unit_rectangle

    _unit_regular_polygons = WeakValueDictionary()

    @classmethod
    def unit_regular_polygon(cls, numVertices):
        """
        Return a :class:`Path` instance for a unit regular
        polygon with the given *numVertices* and radius of 1.0,
        centered at (0, 0).
        """
        if numVertices <= 16:
            path = cls._unit_regular_polygons.get(numVertices)
        else:
            path = None
        if path is None:
            theta = (2 * np.pi / numVertices *
                     np.arange(numVertices + 1).reshape((numVertices + 1, 1)))
            # This initial rotation is to make sure the polygon always
            # "points-up"
            theta += np.pi / 2.0
            verts = np.concatenate((np.cos(theta), np.sin(theta)), 1)
            codes = np.empty((numVertices + 1, ))
            codes[0] = cls.MOVETO
            codes[1:-1] = cls.LINETO
            codes[-1] = cls.CLOSEPOLY
            path = cls(verts, codes, readonly=True)
            if numVertices <= 16:
                cls._unit_regular_polygons[numVertices] = path
        return path

    _unit_regular_stars = WeakValueDictionary()

    @classmethod
    def unit_regular_star(cls, numVertices, innerCircle=0.5):
        """
        Return a :class:`Path` for a unit regular star
        with the given numVertices and radius of 1.0, centered at (0,
        0).
        """
        if numVertices <= 16:
            path = cls._unit_regular_stars.get((numVertices, innerCircle))
        else:
            path = None
        if path is None:
            ns2 = numVertices * 2
            theta = (2 * np.pi / ns2 * np.arange(ns2 + 1))
            # This initial rotation is to make sure the polygon always
            # "points-up"
            theta += np.pi / 2.0
            r = np.ones(ns2 + 1)
            r[1::2] = innerCircle
            verts = np.vstack(
                (r * np.cos(theta), r * np.sin(theta))).transpose()
            codes = np.empty((ns2 + 1, ))
            codes[0] = cls.MOVETO
            codes[1:-1] = cls.LINETO
            codes[-1] = cls.CLOSEPOLY
            path = cls(verts, codes, readonly=True)
            if numVertices <= 16:
                cls._unit_regular_stars[(numVertices, innerCircle)] = path
        return path

    @classmethod
    def unit_regular_asterisk(cls, numVertices):
        """
        Return a :class:`Path` for a unit regular
        asterisk with the given numVertices and radius of 1.0,
        centered at (0, 0).
        """
        return cls.unit_regular_star(numVertices, 0.0)

    _unit_circle = None

    @classmethod
    def unit_circle(cls):
        """
        Return the readonly :class:`Path` of the unit circle.

        For most cases, :func:`Path.circle` will be what you want.

        """
        if cls._unit_circle is None:
            cls._unit_circle = cls.circle(center=(0, 0),
                                          radius=1,
                                          readonly=True)
        return cls._unit_circle

    @classmethod
    def circle(cls, center=(0., 0.), radius=1., readonly=False):
        """
        Return a Path representing a circle of a given radius and center.

        Parameters
        ----------
        center : pair of floats
            The center of the circle. Default ``(0, 0)``.
        radius : float
            The radius of the circle. Default is 1.
        readonly : bool
            Whether the created path should have the "readonly" argument
            set when creating the Path instance.

        Notes
        -----
        The circle is approximated using cubic Bezier curves.  This
        uses 8 splines around the circle using the approach presented
        here:

          Lancaster, Don.  `Approximating a Circle or an Ellipse Using Four
          Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_.

        """
        MAGIC = 0.2652031
        SQRTHALF = np.sqrt(0.5)
        MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0)

        vertices = np.array([
            [0.0, -1.0], [MAGIC, -1.0
                          ], [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45],
            [SQRTHALF, -SQRTHALF], [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45],
            [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC],
            [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], [SQRTHALF, SQRTHALF],
            [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], [MAGIC, 1.0], [0.0, 1.0],
            [-MAGIC, 1.0], [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45],
            [-SQRTHALF, SQRTHALF], [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45],
            [-1.0, MAGIC], [-1.0, 0.0], [-1.0, -MAGIC],
            [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45], [-SQRTHALF, -SQRTHALF],
            [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45], [-MAGIC, -1.0],
            [0.0, -1.0], [0.0, -1.0]
        ],
                            dtype=float)

        codes = [cls.CURVE4] * 26
        codes[0] = cls.MOVETO
        codes[-1] = cls.CLOSEPOLY
        return Path(vertices * radius + center, codes, readonly=readonly)

    _unit_circle_righthalf = None

    @classmethod
    def unit_circle_righthalf(cls):
        """
        Return a :class:`Path` of the right half
        of a unit circle. The circle is approximated using cubic Bezier
        curves.  This uses 4 splines around the circle using the approach
        presented here:

          Lancaster, Don.  `Approximating a Circle or an Ellipse Using Four
          Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_.
        """
        if cls._unit_circle_righthalf is None:
            MAGIC = 0.2652031
            SQRTHALF = np.sqrt(0.5)
            MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0)

            vertices = np.array([[0.0, -1.0], [MAGIC, -1.0],
                                 [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45],
                                 [SQRTHALF, -SQRTHALF],
                                 [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45],
                                 [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC],
                                 [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45],
                                 [SQRTHALF, SQRTHALF],
                                 [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45],
                                 [MAGIC, 1.0], [0.0, 1.0], [0.0, -1.0]], float)

            codes = cls.CURVE4 * np.ones(14)
            codes[0] = cls.MOVETO
            codes[-1] = cls.CLOSEPOLY

            cls._unit_circle_righthalf = cls(vertices, codes, readonly=True)
        return cls._unit_circle_righthalf

    @classmethod
    def arc(cls, theta1, theta2, n=None, is_wedge=False):
        """
        Return an arc on the unit circle from angle
        *theta1* to angle *theta2* (in degrees).

        If *n* is provided, it is the number of spline segments to make.
        If *n* is not provided, the number of spline segments is
        determined based on the delta between *theta1* and *theta2*.

           Masionobe, L.  2003.  `Drawing an elliptical arc using
           polylines, quadratic or cubic Bezier curves
           <http://www.spaceroots.org/documents/ellipse/index.html>`_.
        """
        # degrees to radians
        theta1 *= np.pi / 180.0
        theta2 *= np.pi / 180.0

        twopi = np.pi * 2.0
        halfpi = np.pi * 0.5

        eta1 = np.arctan2(np.sin(theta1), np.cos(theta1))
        eta2 = np.arctan2(np.sin(theta2), np.cos(theta2))
        eta2 -= twopi * np.floor((eta2 - eta1) / twopi)

        # number of curve segments to make
        if n is None:
            n = int(2**np.ceil((eta2 - eta1) / halfpi))
        if n < 1:
            raise ValueError("n must be >= 1 or None")

        deta = (eta2 - eta1) / n
        t = np.tan(0.5 * deta)
        alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0

        steps = np.linspace(eta1, eta2, n + 1, True)
        cos_eta = np.cos(steps)
        sin_eta = np.sin(steps)

        xA = cos_eta[:-1]
        yA = sin_eta[:-1]
        xA_dot = -yA
        yA_dot = xA

        xB = cos_eta[1:]
        yB = sin_eta[1:]
        xB_dot = -yB
        yB_dot = xB

        if is_wedge:
            length = n * 3 + 4
            vertices = np.zeros((length, 2), float)
            codes = cls.CURVE4 * np.ones((length, ), cls.code_type)
            vertices[1] = [xA[0], yA[0]]
            codes[0:2] = [cls.MOVETO, cls.LINETO]
            codes[-2:] = [cls.LINETO, cls.CLOSEPOLY]
            vertex_offset = 2
            end = length - 2
        else:
            length = n * 3 + 1
            vertices = np.empty((length, 2), float)
            codes = cls.CURVE4 * np.ones((length, ), cls.code_type)
            vertices[0] = [xA[0], yA[0]]
            codes[0] = cls.MOVETO
            vertex_offset = 1
            end = length

        vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot
        vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot
        vertices[vertex_offset + 1:end:3, 0] = xB - alpha * xB_dot
        vertices[vertex_offset + 1:end:3, 1] = yB - alpha * yB_dot
        vertices[vertex_offset + 2:end:3, 0] = xB
        vertices[vertex_offset + 2:end:3, 1] = yB

        return cls(vertices, codes, readonly=True)

    @classmethod
    def wedge(cls, theta1, theta2, n=None):
        """
        Return a wedge of the unit circle from angle
        *theta1* to angle *theta2* (in degrees).

        If *n* is provided, it is the number of spline segments to make.
        If *n* is not provided, the number of spline segments is
        determined based on the delta between *theta1* and *theta2*.
        """
        return cls.arc(theta1, theta2, n, True)

    _hatch_dict = maxdict(8)

    @classmethod
    def hatch(cls, hatchpattern, density=6):
        """
        Given a hatch specifier, *hatchpattern*, generates a Path that
        can be used in a repeated hatching pattern.  *density* is the
        number of lines per unit square.
        """
        from matplotlib.hatch import get_path

        if hatchpattern is None:
            return None

        hatch_path = cls._hatch_dict.get((hatchpattern, density))
        if hatch_path is not None:
            return hatch_path

        hatch_path = get_path(hatchpattern, density)
        cls._hatch_dict[(hatchpattern, density)] = hatch_path
        return hatch_path

    def clip_to_bbox(self, bbox, inside=True):
        """
        Clip the path to the given bounding box.

        The path must be made up of one or more closed polygons.  This
        algorithm will not behave correctly for unclosed paths.

        If *inside* is `True`, clip to the inside of the box, otherwise
        to the outside of the box.
        """
        # Use make_compound_path_from_polys
        verts = _path.clip_path_to_rect(self, bbox, inside)
        paths = [Path(poly) for poly in verts]
        return self.make_compound_path(*paths)
Example #24
0
class RendererPS(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles.
    """

    fontd = maxdict(50)
    afmfontd = maxdict(50)

    def __init__(self, width, height, pswriter, imagedpi=72):
        """
        Although postscript itself is dpi independent, we need to
        imform the image code about a requested dpi to generate high
        res images and them scale them before embeddin them
        """
        RendererBase.__init__(self)
        self.width = width
        self.height = height
        self._pswriter = pswriter
        if rcParams['text.usetex']:
            self.textcnt = 0
            self.psfrag = []
        self.imagedpi = imagedpi
        if rcParams['path.simplify']:
            self.simplify = (width * imagedpi, height * imagedpi)
        else:
            self.simplify = None

        # current renderer state (None=uninitialised)
        self.color = None
        self.linewidth = None
        self.linejoin = None
        self.linecap = None
        self.linedash = None
        self.fontname = None
        self.fontsize = None
        self._hatches = {}
        self.image_magnification = imagedpi / 72.0
        self._clip_paths = {}
        self._path_collection_id = 0

        self.used_characters = {}
        self.mathtext_parser = MathTextParser("PS")

    def track_characters(self, font, s):
        """Keeps track of which characters are required from
        each font."""
        realpath, stat_key = get_realpath_and_stat(font.fname)
        used_characters = self.used_characters.setdefault(
            stat_key, (realpath, set()))
        used_characters[1].update([ord(x) for x in s])

    def merge_used_characters(self, other):
        for stat_key, (realpath, charset) in other.items():
            used_characters = self.used_characters.setdefault(
                stat_key, (realpath, set()))
            used_characters[1].update(charset)

    def set_color(self, r, g, b, store=1):
        if (r, g, b) != self.color:
            if r == g and r == b:
                self._pswriter.write("%1.3f setgray\n" % r)
            else:
                self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n" %
                                     (r, g, b))
            if store: self.color = (r, g, b)

    def set_linewidth(self, linewidth, store=1):
        if linewidth != self.linewidth:
            self._pswriter.write("%1.3f setlinewidth\n" % linewidth)
            if store: self.linewidth = linewidth

    def set_linejoin(self, linejoin, store=1):
        if linejoin != self.linejoin:
            self._pswriter.write("%d setlinejoin\n" % linejoin)
            if store: self.linejoin = linejoin

    def set_linecap(self, linecap, store=1):
        if linecap != self.linecap:
            self._pswriter.write("%d setlinecap\n" % linecap)
            if store: self.linecap = linecap

    def set_linedash(self, offset, seq, store=1):
        if self.linedash is not None:
            oldo, oldseq = self.linedash
            if seq_allequal(seq, oldseq): return

        if seq is not None and len(seq):
            s = "[%s] %d setdash\n" % (_nums_to_str(*seq), offset)
            self._pswriter.write(s)
        else:
            self._pswriter.write("[] 0 setdash\n")
        if store: self.linedash = (offset, seq)

    def set_font(self, fontname, fontsize, store=1):
        if rcParams['ps.useafm']: return
        if (fontname, fontsize) != (self.fontname, self.fontsize):
            out = ("/%s findfont\n"
                   "%1.3f scalefont\n"
                   "setfont\n" % (fontname, fontsize))

            self._pswriter.write(out)
            if store: self.fontname = fontname
            if store: self.fontsize = fontsize

    def create_hatch(self, hatch):
        sidelen = 72
        if self._hatches.has_key(hatch):
            return self._hatches[hatch]
        name = 'H%d' % len(self._hatches)
        self._pswriter.write("""\
  << /PatternType 1
     /PaintType 2
     /TilingType 2
     /BBox[0 0 %(sidelen)d %(sidelen)d]
     /XStep %(sidelen)d
     /YStep %(sidelen)d

     /PaintProc {
        pop
        0 setlinewidth
""" % locals())
        self._pswriter.write(
            self._convert_path(Path.hatch(hatch),
                               Affine2D().scale(72.0)))
        self._pswriter.write("""\
          stroke
     } bind
   >>
   matrix
   makepattern
   /%(name)s exch def
""" % locals())
        self._hatches[hatch] = name
        return name

    def get_canvas_width_height(self):
        'return the canvas width and height in display coords'
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        """
        get the width and height in display coords of the string s
        with FontPropertry prop

        """
        if rcParams['text.usetex']:
            texmanager = self.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s,
                                                               fontsize,
                                                               renderer=self)
            return w, h, d

        if ismath:
            width, height, descent, pswriter, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width, height, descent

        if rcParams['ps.useafm']:
            if ismath: s = s[1:-1]
            font = self._get_font_afm(prop)
            l, b, w, h, d = font.get_str_bbox_and_descent(s)

            fontsize = prop.get_size_in_points()
            scale = 0.001 * fontsize
            w *= scale
            h *= scale
            d *= scale
            return w, h, d

        font = self._get_font_ttf(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        w, h = font.get_width_height()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d = font.get_descent()
        d /= 64.0
        #print s, w, h
        return w, h, d

    def flipy(self):
        'return true if small y numbers are top for renderer'
        return False

    def _get_font_afm(self, prop):
        key = hash(prop)
        font = self.afmfontd.get(key)
        if font is None:
            fname = findfont(prop, fontext='afm')
            font = self.afmfontd.get(fname)
            if font is None:
                font = AFM(file(findfont(prop, fontext='afm')))
                self.afmfontd[fname] = font
            self.afmfontd[key] = font
        return font

    def _get_font_ttf(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = self.fontd.get(fname)
            if font is None:
                font = FT2Font(str(fname))
                self.fontd[fname] = font
            self.fontd[key] = font
        font.clear()
        size = prop.get_size_in_points()
        font.set_size(size, 72.0)
        return font

    def _rgba(self, im):
        return im.as_rgba_str()

    def _rgb(self, im):
        h, w, s = im.as_rgba_str()

        rgba = npy.fromstring(s, npy.uint8)
        rgba.shape = (h, w, 4)
        rgb = rgba[:, :, :3]
        return h, w, rgb.tostring()

    def _gray(self, im, rc=0.3, gc=0.59, bc=0.11):
        rgbat = im.as_rgba_str()
        rgba = npy.fromstring(rgbat[2], npy.uint8)
        rgba.shape = (rgbat[0], rgbat[1], 4)
        rgba_f = rgba.astype(npy.float32)
        r = rgba_f[:, :, 0]
        g = rgba_f[:, :, 1]
        b = rgba_f[:, :, 2]
        gray = (r * rc + g * gc + b * bc).astype(npy.uint8)
        return rgbat[0], rgbat[1], gray.tostring()

    def _hex_lines(self, s, chars_per_line=128):
        s = binascii.b2a_hex(s)
        nhex = len(s)
        lines = []
        for i in range(0, nhex, chars_per_line):
            limit = min(i + chars_per_line, nhex)
            lines.append(s[i:limit])
        return lines

    def get_image_magnification(self):
        """
        Get the factor by which to magnify images passed to draw_image.
        Allows a backend to have images at a different resolution to other
        artists.
        """
        return self.image_magnification

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        """
        Draw the Image instance into the current axes; x is the
        distance in pixels from the left hand side of the canvas and y
        is the distance from bottom

        bbox is a matplotlib.transforms.BBox instance for clipping, or
        None
        """

        im.flipud_out()

        if im.is_grayscale:
            h, w, bits = self._gray(im)
            imagecmd = "image"
        else:
            h, w, bits = self._rgb(im)
            imagecmd = "false 3 colorimage"
        hexlines = '\n'.join(self._hex_lines(bits))

        xscale, yscale = (w / self.image_magnification,
                          h / self.image_magnification)

        figh = self.height * 72
        #print 'values', origin, flipud, figh, h, y

        clip = []
        if bbox is not None:
            clipx, clipy, clipw, cliph = bbox.bounds
            clip.append('%s clipbox' %
                        _nums_to_str(clipw, cliph, clipx, clipy))
        if clippath is not None:
            id = self._get_clip_path(clippath, clippath_trans)
            clip.append('%s' % id)
        clip = '\n'.join(clip)

        #y = figh-(y+h)
        ps = """gsave
%(clip)s
%(x)s %(y)s translate
%(xscale)s %(yscale)s scale
/DataString %(w)s string def
%(w)s %(h)s 8 [ %(w)s 0 0 -%(h)s 0 %(h)s ]
{
currentfile DataString readhexstring pop
} bind %(imagecmd)s
%(hexlines)s
grestore
""" % locals()
        self._pswriter.write(ps)

        # unflip
        im.flipud_out()

    def _convert_path(self, path, transform, simplify=None):
        path = transform.transform_path(path)

        ps = []
        last_points = None
        for points, code in path.iter_segments(simplify):
            if code == Path.MOVETO:
                ps.append("%g %g m" % tuple(points))
            elif code == Path.LINETO:
                ps.append("%g %g l" % tuple(points))
            elif code == Path.CURVE3:
                points = quad2cubic(*(list(last_points[-2:]) + list(points)))
                ps.append("%g %g %g %g %g %g c" % tuple(points[2:]))
            elif code == Path.CURVE4:
                ps.append("%g %g %g %g %g %g c" % tuple(points))
            elif code == Path.CLOSEPOLY:
                ps.append("cl")
            last_points = points

        ps = "\n".join(ps)
        return ps

    def _get_clip_path(self, clippath, clippath_transform):
        id = self._clip_paths.get((clippath, clippath_transform))
        if id is None:
            id = 'c%x' % len(self._clip_paths)
            ps_cmd = ['/%s {' % id]
            ps_cmd.append(self._convert_path(clippath, clippath_transform))
            ps_cmd.extend(['clip', 'newpath', '} bind def\n'])
            self._pswriter.write('\n'.join(ps_cmd))
            self._clip_paths[(clippath, clippath_transform)] = id
        return id

    def draw_path(self, gc, path, transform, rgbFace=None):
        """
        Draws a Path instance using the given affine transform.
        """
        ps = self._convert_path(path, transform, self.simplify)
        self._draw_ps(ps, gc, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        """
        Draw the markers defined by path at each of the positions in x
        and y.  path coordinates are points, x and y coords will be
        transformed by the transform
        """
        if debugPS: self._pswriter.write('% draw_markers \n')

        write = self._pswriter.write

        if rgbFace:
            if rgbFace[0] == rgbFace[1] and rgbFace[0] == rgbFace[2]:
                ps_color = '%1.3f setgray' % rgbFace[0]
            else:
                ps_color = '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace

        # construct the generic marker command:
        ps_cmd = ['/o {', 'gsave', 'newpath',
                  'translate']  # dont want the translate to be global
        ps_cmd.append(self._convert_path(marker_path, marker_trans))

        if rgbFace:
            ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore'])

        ps_cmd.extend(['stroke', 'grestore', '} bind def'])

        tpath = trans.transform_path(path)
        for vertices, code in tpath.iter_segments():
            if len(vertices):
                x, y = vertices[-2:]
                ps_cmd.append("%g %g o" % (x, y))

        ps = '\n'.join(ps_cmd)
        self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False)

    def draw_path_collection(self, master_transform, cliprect, clippath,
                             clippath_trans, paths, all_transforms, offsets,
                             offsetTrans, facecolors, edgecolors, linewidths,
                             linestyles, antialiaseds, urls):
        write = self._pswriter.write

        path_codes = []
        for i, (path, transform) in enumerate(
                self._iter_collection_raw_paths(master_transform, paths,
                                                all_transforms)):
            name = 'p%x_%x' % (self._path_collection_id, i)
            ps_cmd = ['/%s {' % name, 'newpath', 'translate']
            ps_cmd.append(self._convert_path(path, transform))
            ps_cmd.extend(['} bind def\n'])
            write('\n'.join(ps_cmd))
            path_codes.append(name)

        for xo, yo, path_id, gc, rgbFace in self._iter_collection(
                path_codes, cliprect, clippath, clippath_trans, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds, urls):

            ps = "%g %g %s" % (xo, yo, path_id)
            self._draw_ps(ps, gc, rgbFace)

        self._path_collection_id += 1

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'):
        """
        draw a Text instance
        """
        w, h, bl = self.get_text_width_height_descent(s, prop, ismath)
        fontsize = prop.get_size_in_points()
        corr = 0  #w/2*(fontsize-10)/10
        pos = _nums_to_str(x - corr, y)
        thetext = 'psmarker%d' % self.textcnt
        color = '%1.3f,%1.3f,%1.3f' % gc.get_rgb()[:3]
        fontcmd = {
            'sans-serif': r'{\sffamily %s}',
            'monospace': r'{\ttfamily %s}'
        }.get(rcParams['font.family'], r'{\rmfamily %s}')
        s = fontcmd % s
        tex = r'\color[rgb]{%s} %s' % (color, s)
        self.psfrag.append(r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' %
                           (thetext, angle, fontsize, fontsize * 1.25, tex))
        ps = """\
gsave
%(pos)s moveto
(%(thetext)s)
show
grestore
    """ % locals()

        self._pswriter.write(ps)
        self.textcnt += 1

    def draw_text(self, gc, x, y, s, prop, angle, ismath):
        """
        draw a Text instance
        """
        # local to avoid repeated attribute lookups

        write = self._pswriter.write
        if debugPS:
            write("% text\n")

        if ismath == 'TeX':
            return self.tex(gc, x, y, s, prop, angle)

        elif ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)

        elif isinstance(s, unicode):
            return self.draw_unicode(gc, x, y, s, prop, angle)

        elif rcParams['ps.useafm']:
            font = self._get_font_afm(prop)

            l, b, w, h = font.get_str_bbox(s)

            fontsize = prop.get_size_in_points()
            l *= 0.001 * fontsize
            b *= 0.001 * fontsize
            w *= 0.001 * fontsize
            h *= 0.001 * fontsize

            if angle == 90: l, b = -b, l  # todo generalize for arb rotations

            pos = _nums_to_str(x - l, y - b)
            thetext = '(%s)' % s
            fontname = font.get_fontname()
            fontsize = prop.get_size_in_points()
            rotate = '%1.1f rotate' % angle
            setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()[:3]
            #h = 0
            ps = """\
gsave
/%(fontname)s findfont
%(fontsize)s scalefont
setfont
%(pos)s moveto
%(rotate)s
%(thetext)s
%(setcolor)s
show
grestore
    """ % locals()
            self._draw_ps(ps, gc, None)

        else:
            font = self._get_font_ttf(prop)
            font.set_text(s, 0, flags=LOAD_NO_HINTING)
            self.track_characters(font, s)

            self.set_color(*gc.get_rgb())
            self.set_font(font.get_sfnt()[(1, 0, 0, 6)],
                          prop.get_size_in_points())
            write("%s m\n" % _nums_to_str(x, y))
            if angle:
                write("gsave\n")
                write("%s rotate\n" % _num_to_str(angle))
            descent = font.get_descent() / 64.0
            if descent:
                write("0 %s rmoveto\n" % _num_to_str(descent))
            write("(%s) show\n" % quote_ps_string(s))
            if angle:
                write("grestore\n")

    def new_gc(self):
        return GraphicsContextPS()

    def draw_unicode(self, gc, x, y, s, prop, angle):
        """draw a unicode string.  ps doesn't have unicode support, so
        we have to do this the hard way
        """
        if rcParams['ps.useafm']:
            self.set_color(*gc.get_rgb())

            font = self._get_font_afm(prop)
            fontname = font.get_fontname()
            fontsize = prop.get_size_in_points()
            scale = 0.001 * fontsize

            thisx = 0
            thisy = font.get_str_bbox_and_descent(s)[4] * scale
            last_name = None
            lines = []
            for c in s:
                name = uni2type1.get(ord(c), 'question')
                try:
                    width = font.get_width_from_char_name(name)
                except KeyError:
                    name = 'question'
                    width = font.get_width_char('?')
                if last_name is not None:
                    kern = font.get_kern_dist_from_name(last_name, name)
                else:
                    kern = 0
                last_name = name
                thisx += kern * scale

                lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name))

                thisx += width * scale

            thetext = "\n".join(lines)
            ps = """\
gsave
/%(fontname)s findfont
%(fontsize)s scalefont
setfont
%(x)f %(y)f translate
%(angle)f rotate
%(thetext)s
grestore
    """ % locals()
            self._pswriter.write(ps)

        else:
            font = self._get_font_ttf(prop)
            font.set_text(s, 0, flags=LOAD_NO_HINTING)
            self.track_characters(font, s)

            self.set_color(*gc.get_rgb())
            self.set_font(font.get_sfnt()[(1, 0, 0, 6)],
                          prop.get_size_in_points())

            cmap = font.get_charmap()
            lastgind = None
            #print 'text', s
            lines = []
            thisx = 0
            thisy = font.get_descent() / 64.0
            for c in s:
                ccode = ord(c)
                gind = cmap.get(ccode)
                if gind is None:
                    ccode = ord('?')
                    name = '.notdef'
                    gind = 0
                else:
                    name = font.get_glyph_name(gind)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)

                if lastgind is not None:
                    kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
                else:
                    kern = 0
                lastgind = gind
                thisx += kern / 64.0

                lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name))
                thisx += glyph.linearHoriAdvance / 65536.0

            thetext = '\n'.join(lines)
            ps = """gsave
%(x)f %(y)f translate
%(angle)f rotate
%(thetext)s
grestore
""" % locals()
            self._pswriter.write(ps)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        if debugPS:
            self._pswriter.write("% mathtext\n")

        width, height, descent, pswriter, used_characters = \
            self.mathtext_parser.parse(s, 72, prop)
        self.merge_used_characters(used_characters)
        self.set_color(*gc.get_rgb())
        thetext = pswriter.getvalue()
        ps = """gsave
%(x)f %(y)f translate
%(angle)f rotate
%(thetext)s
grestore
""" % locals()
        self._pswriter.write(ps)

    def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None):
        """
        Emit the PostScript sniplet 'ps' with all the attributes from 'gc'
        applied.  'ps' must consist of PostScript commands to construct a path.

        The fill and/or stroke kwargs can be set to False if the
        'ps' string already includes filling and/or stroking, in
        which case _draw_ps is just supplying properties and
        clipping.
        """
        # local variable eliminates all repeated attribute lookups
        write = self._pswriter.write
        if debugPS and command:
            write("% " + command + "\n")
        mightstroke = (gc.get_linewidth() > 0.0
                       and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0))
        stroke = stroke and mightstroke
        fill = (fill and rgbFace is not None
                and (len(rgbFace) <= 3 or rgbFace[3] != 0.0))

        if mightstroke:
            self.set_linewidth(gc.get_linewidth())
            jint = gc.get_joinstyle()
            self.set_linejoin(jint)
            cint = gc.get_capstyle()
            self.set_linecap(cint)
            self.set_linedash(*gc.get_dashes())
            self.set_color(*gc.get_rgb()[:3])
        write('gsave\n')

        cliprect = gc.get_clip_rectangle()
        if cliprect:
            x, y, w, h = cliprect.bounds
            write('%1.4g %1.4g %1.4g %1.4g clipbox\n' % (w, h, x, y))
        clippath, clippath_trans = gc.get_clip_path()
        if clippath:
            id = self._get_clip_path(clippath, clippath_trans)
            write('%s\n' % id)

        # Jochen, is the strip necessary? - this could be a honking big string
        write(ps.strip())
        write("\n")

        if fill:
            if stroke:
                write("gsave\n")
            self.set_color(store=0, *rgbFace[:3])
            write("fill\n")
            if stroke:
                write("grestore\n")

        hatch = gc.get_hatch()
        if hatch:
            hatch_name = self.create_hatch(hatch)
            write("gsave\n")
            write("[/Pattern [/DeviceRGB]] setcolorspace %f %f %f " %
                  gc.get_rgb()[:3])
            write("%s setcolor fill grestore\n" % hatch_name)

        if stroke:
            write("stroke\n")

        write("grestore\n")
class RendererMac(RendererBase):
    """
    The renderer handles drawing/rendering operations. Most of the renderer's
    methods forward the command to the renderer's graphics context. The
    renderer does not wrap a C object and is written in pure Python.
    """

    texd = maxdict(50)  # a cache of tex image rasters

    def __init__(self, dpi, width, height):
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        self.gc = GraphicsContextMac()
        self.gc.set_dpi(self.dpi)
        self.mathtext_parser = MathTextParser('MacOSX')

    def set_width_height (self, width, height):
        self.width, self.height = width, height

    def draw_path(self, gc, path, transform, rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        linewidth = gc.get_linewidth()
        gc.draw_path(path, transform, linewidth, rgbFace)

    def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        linewidth = gc.get_linewidth()
        gc.draw_markers(marker_path, marker_trans, path, trans, linewidth, rgbFace)

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):
        if offset_position=='data':
            offset_position = True
        else:
            offset_position = False
        path_ids = []
        for path, transform in self._iter_collection_raw_paths(
            master_transform, paths, all_transforms):
            path_ids.append((path, transform))
        master_transform = master_transform.get_matrix()
        all_transforms = [t.get_matrix() for t in all_transforms]
        offsetTrans = offsetTrans.get_matrix()
        gc.draw_path_collection(master_transform, path_ids, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds,
                             offset_position)

    def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
                       coordinates, offsets, offsetTrans, facecolors,
                       antialiased, edgecolors):
        gc.draw_quad_mesh(master_transform.get_matrix(),
                          meshWidth,
                          meshHeight,
                          coordinates,
                          offsets,
                          offsetTrans.get_matrix(),
                          facecolors,
                          antialiased,
                          edgecolors)

    def new_gc(self):
        self.gc.save()
        self.gc.set_hatch(None)
        self.gc._alpha = 1.0
        self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
        return self.gc

    def draw_gouraud_triangle(self, gc, points, colors, transform):
        points = transform.transform(points)
        gc.draw_gouraud_triangle(points, colors)

    def draw_image(self, gc, x, y, im):
        im.flipud_out()
        nrows, ncols, data = im.as_rgba_str()
        gc.draw_image(x, y, nrows, ncols, data)
        im.flipud_out()

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()
        texmanager = self.get_texmanager()
        key = s, size, self.dpi, angle, texmanager.get_font_config()
        im = self.texd.get(key) # Not sure what this does; just copied from backend_agg.py
        if im is None:
            Z = texmanager.get_grey(s, size, self.dpi)
            Z = numpy.array(255.0 - Z * 255.0, numpy.uint8)

        gc.draw_mathtext(x, y, angle, Z)

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ox, oy, width, height, descent, image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi, prop)
        gc.draw_mathtext(x, y, angle, 255 - image.as_array())

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
            family =  prop.get_family()
            weight = prop.get_weight()
            style = prop.get_style()
            points = prop.get_size_in_points()
            size = self.points_to_pixels(points)
            gc.draw_text(x, y, unicode(s), family, size, weight, style, angle)

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath=='TeX':
            # todo: handle props
            texmanager = self.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s, fontsize,
                                                               renderer=self)
            return w, h, d
        if ismath:
            ox, oy, width, height, descent, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent
        family =  prop.get_family()
        weight = prop.get_weight()
        style = prop.get_style()
        points = prop.get_size_in_points()
        size = self.points_to_pixels(points)
        width, height, descent = self.gc.get_text_width_height_descent(unicode(s), family, size, weight, style)
        return  width, height, 0.0*descent

    def flipy(self):
        return False

    def points_to_pixels(self, points):
        return points/72.0 * self.dpi

    def option_image_nocomposite(self):
        return True
Example #26
0
class RendererMac(RendererBase):
    """
    The renderer handles drawing/rendering operations. Most of the renderer's
    methods forwards the command to the renderer's graphics context. The
    renderer does not wrap a C object and is written in pure Python.
    """

    texd = maxdict(50)  # a cache of tex image rasters

    def __init__(self, dpi, width, height):
        RendererBase.__init__(self)
        self.dpi = dpi
        self.width = width
        self.height = height
        self.gc = GraphicsContextMac()

    def set_width_height(self, width, height):
        self.width, self.height = width, height

    def draw_path(self, gc, path, transform, rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        gc.draw_path(path, transform, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        gc.draw_markers(marker_path, marker_trans, path, trans, rgbFace)

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_transform = gc.get_clip_path()
        gc.draw_path_collection(master_transform, cliprect, clippath,
                                clippath_transform, paths, all_transforms,
                                offsets, offsetTrans, facecolors, edgecolors,
                                linewidths, linestyles, antialiaseds)

    def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
                       coordinates, offsets, offsetTrans, facecolors,
                       antialiased, showedges):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_transform = gc.get_clip_path()
        gc.draw_quad_mesh(master_transform, cliprect, clippath,
                          clippath_transform, meshWidth, meshHeight,
                          coordinates, offsets, offsetTrans, facecolors,
                          antialiased, showedges)

    def new_gc(self):
        self.gc.save()
        self.gc.set_hatch(None)
        return self.gc

    def draw_image(self, gc, x, y, im):
        im.flipud_out()
        nrows, ncols, data = im.as_rgba_str()
        gc.draw_image(x, y, nrows, ncols, data, gc.get_clip_rectangle(),
                      *gc.get_clip_path())
        im.flipud_out()

    def draw_tex(self, gc, x, y, s, prop, angle):
        # todo, handle props, angle, origins
        size = prop.get_size_in_points()
        texmanager = self.get_texmanager()
        key = s, size, self.dpi, angle, texmanager.get_font_config()
        im = self.texd.get(
            key)  # Not sure what this does; just copied from backend_agg.py
        if im is None:
            Z = texmanager.get_grey(s, size, self.dpi)
            Z = numpy.array(255.0 - Z * 255.0, numpy.uint8)

        gc.draw_mathtext(x, y, angle, Z)

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if not HAVE_MATHTEX:
            return

        m = Mathtex(s,
                    rcParams['mathtext.fontset'],
                    prop.get_size_in_points(),
                    self.dpi,
                    rcParams['mathtext.default'],
                    cache=True)
        b = MathtexBackendImage()
        m.render_to_backend(b)

        gc.draw_mathtext(x, y, angle, 255 - b.image.as_array())

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
            family = prop.get_family()
            weight = prop.get_weight()
            style = prop.get_style()
            points = prop.get_size_in_points()
            size = self.points_to_pixels(points)
            gc.draw_text(x, y, unicode(s), family, size, weight, style, angle)

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath == 'TeX':
            # todo: handle props
            texmanager = self.get_texmanager()
            fontsize = prop.get_size_in_points()
            w, h, d = texmanager.get_text_width_height_descent(s,
                                                               fontsize,
                                                               renderer=self)
            return w, h, d
        if ismath:
            if HAVE_MATHTEX:
                m = Mathtex(s,
                            rcParams['mathtext.fontset'],
                            prop.get_size_in_points(),
                            self.dpi,
                            rcParams['mathtext.default'],
                            cache=True)
                return m.width, m.height, m.depth
            else:
                warnings.warn(
                    'matplotlib was compiled without mathtex support. ' +
                    'Math will not be rendered.')
                return 0.0, 0.0, 0.0
        family = prop.get_family()
        weight = prop.get_weight()
        style = prop.get_style()
        points = prop.get_size_in_points()
        size = self.points_to_pixels(points)
        width, height, descent = self.gc.get_text_width_height_descent(
            unicode(s), family, size, weight, style)
        return width, height, 0.0 * descent

    def flipy(self):
        return False

    def points_to_pixels(self, points):
        return points / 72.0 * self.dpi

    def option_image_nocomposite(self):
        return True