예제 #1
0
def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
    """
    Given a math expression, renders it in a closely-clipped bounding
    box to an image file.

    *s*
       A math expression.  The math portion should be enclosed in
       dollar signs.

    *filename_or_obj*
       A filepath or writable file-like object to write the image data
       to.

    *prop*
       If provided, a FontProperties() object describing the size and
       style of the text.

    *dpi*
       Override the output dpi, otherwise use the default associated
       with the output format.

    *format*
       The output format, eg. 'svg', 'pdf', 'ps' or 'png'.  If not
       provided, will be deduced from the filename.
    """
    from matplotlib import figure

    # backend_agg supports all of the core output formats
    from matplotlib.backends import backend_agg
    from matplotlib.font_manager import FontProperties
    from matplotlib.mathtext import MathTextParser

    if prop is None:
        prop = FontProperties()

    parser = MathTextParser("path")
    width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)

    fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
    fig.text(0, depth / height, s, fontproperties=prop)
    backend_agg.FigureCanvasAgg(fig)
    fig.savefig(filename_or_obj, dpi=dpi, format=format)

    return depth
예제 #2
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()
예제 #3
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
        if rcParams["path.simplify"]:
            self.simplify = (width, height)
        else:
            self.simplify = None

        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

        if gc.get_url() is not None:
            self._svgwriter.write('<a xlink:href="%s">' % gc.get_url())
        style = self._get_style(gc, rgbFace)
        self._svgwriter.write('<%s style="%s" %s %s/>\n' % (element, style, clippath, details))
        if gc.get_url() is not None:
            self._svgwriter.write("</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_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: %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()[:3]),
                    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, 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)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % 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%f %f",
        Path.LINETO: "L%f %f",
        Path.CURVE3: "Q%f %f %f %f",
        Path.CURVE4: "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, simplify=None):
        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(simplify):
            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.simplify)
        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 vertices, code in tpath.iter_segments():
            if len(vertices):
                x, y = vertices[-2:]
                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,
        urls,
    ):
        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,
            urls,
        ):
            clipid = self._get_gc_clip_svg(gc)
            url = gc.get_url()
            if url is not None:
                self._svgwriter.write('<a xlink:href="%s">' % url)
            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>")
            if url is not None:
                self._svgwriter.write("</a>")

        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())
            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()

        url = getattr(im, "_url", None)
        if url is not None:
            self._svgwriter.write('<a xlink:href="%s">' % url)
        self._svgwriter.write(
            '<image x="%f" y="%f" width="%f" height="%f" '
            '%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')
        if url is not None:
            self._svgwriter.write("</a>")

    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: %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
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' x="%f"' % (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: %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(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(%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 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
예제 #4
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'regular'    : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
           cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        self.mathtext_parser = MathTextParser('Cairo')
        RendererBase.__init__(self)

    def set_ctx_from_surface(self, surface):
        self.gc.ctx = cairo.Context(surface)
        # Although it may appear natural to automatically call
        # `self.set_width_height(surface.get_width(), surface.get_height())`
        # here (instead of having the caller do so separately), this would fail
        # for PDF/PS/SVG surfaces, which have no way to report their extents.

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

    def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    @cbook.deprecated("3.0")
    def convert_path(ctx, path, transform, clip=None):
        _append_path(ctx, path, transform, clip)

    def draw_path(self, gc, path, transform, rgbFace=None):
        ctx = gc.ctx
        # Clip the path to the actual rendering extents if it isn't filled.
        clip = (ctx.clip_extents()
                if rgbFace is None and gc.get_hatch() is None
                else None)
        transform = (transform
                     + Affine2D().scale(1, -1).translate(0, self.height))
        ctx.new_path()
        _append_path(ctx, path, transform, clip)
        self._fill_and_stroke(
            ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_markers(self, gc, marker_path, marker_trans, path, transform,
                     rgbFace=None):
        ctx = gc.ctx

        ctx.new_path()
        # Create the path for the marker; it needs to be flipped here already!
        _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
        marker_path = ctx.copy_path_flat()

        # Figure out whether the path has a fill
        x1, y1, x2, y2 = ctx.fill_extents()
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            filled = False
            # No fill, just unset this (so we don't try to fill it later on)
            rgbFace = None
        else:
            filled = True

        transform = (transform
                     + Affine2D().scale(1, -1).translate(0, self.height))

        ctx.new_path()
        for i, (vertices, codes) in enumerate(
                path.iter_segments(transform, simplify=False)):
            if len(vertices):
                x, y = vertices[-2:]
                ctx.save()

                # Translate and apply path
                ctx.translate(x, y)
                ctx.append_path(marker_path)

                ctx.restore()

                # Slower code path if there is a fill; we need to draw
                # the fill and stroke for each marker at the same time.
                # Also flush out the drawing every once in a while to
                # prevent the paths from getting way too long.
                if filled or i % 1000 == 0:
                    self._fill_and_stroke(
                        ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

        # Fast path, if there is no fill, draw everything in one step
        if not filled:
            self._fill_and_stroke(
                ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_path_collection(
            self, gc, master_transform, paths, all_transforms, offsets,
            offsetTrans, facecolors, edgecolors, linewidths, linestyles,
            antialiaseds, urls, offset_position):

        path_ids = []
        for path, transform in self._iter_collection_raw_paths(
                master_transform, paths, all_transforms):
            path_ids.append((path, Affine2D(transform)))

        reuse_key = None
        grouped_draw = []

        def _draw_paths():
            if not grouped_draw:
                return
            gc_vars, rgb_fc = reuse_key
            gc = copy.copy(gc0)
            # We actually need to call the setters to reset the internal state.
            vars(gc).update(gc_vars)
            for k, v in gc_vars.items():
                if k == "_linestyle":  # Deprecated, no effect.
                    continue
                try:
                    getattr(gc, "set" + k)(v)
                except (AttributeError, TypeError) as e:
                    pass
            gc.ctx.new_path()
            paths, transforms = zip(*grouped_draw)
            grouped_draw.clear()
            _append_paths(gc.ctx, paths, transforms)
            self._fill_and_stroke(
                gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha())

        for xo, yo, path_id, gc0, rgb_fc in self._iter_collection(
                gc, master_transform, all_transforms, path_ids, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds, urls, offset_position):
            path, transform = path_id
            transform = (Affine2D(transform.get_matrix())
                         .translate(xo, yo - self.height).scale(1, -1))
            # rgb_fc could be a ndarray, for which equality is elementwise.
            new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None
            if new_key == reuse_key:
                grouped_draw.append((path, transform))
            else:
                _draw_paths()
                grouped_draw.append((path, transform))
                reuse_key = new_key
        _draw_paths()

    def draw_image(self, gc, x, y, im):
        im = cbook._unmultipled_rgba8888_to_premultiplied_argb32(im[::-1])
        surface = cairo.ImageSurface.create_for_data(
            im.ravel().data, cairo.FORMAT_ARGB32,
            im.shape[1], im.shape[0], im.shape[1] * 4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to(x, y)
            ctx.select_font_face(prop.get_name(),
                                 self.fontangles[prop.get_style()],
                                 self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate(np.deg2rad(-angle))
            ctx.set_font_size(size)

            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate(np.deg2rad(-angle))

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.select_font_face(fontProp.name,
                                 self.fontangles[fontProp.style],
                                 self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            ctx.show_text(s)

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle(ox, oy, w, h)
            ctx.set_source_rgb(0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()

    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, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face(prop.get_name(),
                             self.fontangles[prop.get_style()],
                             self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small
        size = prop.get_size_in_points() * self.dpi / 72

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size(size)

        y_bearing, w, h = ctx.text_extents(s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing

    def new_gc(self):
        self.gc.ctx.save()
        self.gc._alpha = 1
        self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
        return self.gc

    def points_to_pixels(self, points):
        return points / 72 * self.dpi
예제 #5
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)
예제 #6
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 = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        if gc.get_url() is not None:
            self._svgwriter.write('<a xlink:href="%s">' % gc.get_url())
        style = self._get_style(gc, rgbFace)
        self._svgwriter.write ('<%s style="%s" %s %s/>\n' % (
                element, style, clippath, details))
        if gc.get_url() is not None:
            self._svgwriter.write('</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(str(dictkey)).hexdigest()
            self._svgwriter.write('<defs>\n  <pattern id="%s" ' % id)
            self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ')
            self._svgwriter.write(' 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(
                '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' %
                (HATCH_SIZE+1, HATCH_SIZE+1, fill))
            path = '<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % (
                path_data, rgb2hex(gc.get_rgb()[:3]), rgb2hex(gc.get_rgb()[:3]))
            self._svgwriter.write(path)
            self._svgwriter.write('\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 = "url(#%s)" % self._get_hatch(gc, rgbFace)
        else:
            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: %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()[:3]),
                         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, 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 = '<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)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % 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, 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('<g id="%s">\n' % (gid))
        else:
            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%f %f',
        Path.LINETO: 'L%f %f',
        Path.CURVE3: 'Q%f %f %f %f',
        Path.CURVE4: '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 = '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)
        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('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),
                                 simplify=False)
        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)
        for vertices, code in path.iter_segments(trans_and_flip, simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                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, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls):
        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, simplify=False)
            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, 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('<a xlink:href="%s">' % url)
            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(gc0, rgbFace)
            self._svgwriter.write ('<use style="%s" %s/>\n' % (style, details))
            if clipid is not None:
                write('</g>')
            if url is not None:
                self._svgwriter.write('</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.

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

        write('<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][:3]

            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('<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">' %
                  (self._n_gradients, i, x1, y1, xb, yb))
            write('<stop offset="0" stop-color="%s" stop-opacity="1.0"/>' % rgb2hex(c))
            write('<stop offset="1" stop-color="%s" stop-opacity="0.0"/>' % rgb2hex(c))
            write('</linearGradient>')

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

        avg_color = np.sum(colors[:, :3], axis=0) / 3.0
        write('<use xlink:href="#GT%x" fill="%s"/>\n' %
              (self._n_gradients, rgb2hex(avg_color)))
        for i in range(3):
            write('<use xlink:href="#GT%x" fill="url(#GR%x_%d)" 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 = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        write('<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('</g>\n')

    def draw_image(self, gc, x, y, im):
        # MGDTODO: Support clippath here
        trans = [1,0,0,1,0,0]
        transstr = ''
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            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()

        url = getattr(im, '_url', None)
        if url is not None:
            self._svgwriter.write('<a xlink:href="%s">' % url)
        self._svgwriter.write (
            '<image x="%f" y="%f" width="%f" height="%f" '
            '%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')
        if url is not None:
            self._svgwriter.write('</a>')

    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):
        """
        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()[:3])
        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('<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 = '<path id="%s" d="%s"/>\n' % (char_id, ''.join(path_data))
                    write(path_element)
                write('</defs>\n')

                glyph_map.update(glyph_map_new)

            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: %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 / text2path.FONT_SCALE))

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

            svg.append('</g>\n')
            if clipid is not None:
                svg.append('</g>\n')
            svg = ''.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('<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 = '<path id="%s" d="%s"/>\n' % (char_id, ''.join(path_data))
                    write(path_element)
                write('</defs>\n')

                glyph_map.update(glyph_map_new)

            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: %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,-%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('<use xlink:href="#%s"' % char_id)
                svg.append(' x="%f" y="%f" transform="scale(%f)"' % (xposition/scale,
                                                                      yposition/scale,
                                                                      scale))
                svg.append('/>\n')


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


            svg.append('</g><!-- style -->\n')
            if clipid is not None:
                svg.append('</g><!-- clipid -->\n')
            svg = ''.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()[: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: %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
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' x="%f"' %
                               (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: %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(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(%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 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 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
예제 #7
0
class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles.
    """

    _afm_font_dir = cbook._get_data_path("fonts/afm")
    _use_afm_rc_name = "ps.useafm"

    def __init__(self, width, height, pswriter, imagedpi=72):
        # Although postscript itself is dpi independent, we need to inform the
        # image code about a requested dpi to generate high resolution images
        # and them scale them before embedding them.
        super().__init__(width, height)
        self._pswriter = pswriter
        if mpl.rcParams['text.usetex']:
            self.textcnt = 0
            self.psfrag = []
        self.imagedpi = imagedpi

        # 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
        self._clip_paths = {}
        self._path_collection_id = 0

        self._character_tracker = _backend_pdf_ps.CharacterTracker()
        self.mathtext_parser = MathTextParser("PS")

    @cbook.deprecated("3.3")
    @property
    def used_characters(self):
        return self._character_tracker.used_characters

    @cbook.deprecated("3.3")
    def track_characters(self, *args, **kwargs):
        """Keep track of which characters are required from each font."""
        self._character_tracker.track(*args, **kwargs)

    @cbook.deprecated("3.3")
    def merge_used_characters(self, *args, **kwargs):
        self._character_tracker.merge(*args, **kwargs)

    def set_color(self, r, g, b, store=True):
        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=True):
        linewidth = float(linewidth)
        if linewidth != self.linewidth:
            self._pswriter.write("%1.3f setlinewidth\n" % linewidth)
            if store:
                self.linewidth = linewidth

    def set_linejoin(self, linejoin, store=True):
        if linejoin != self.linejoin:
            self._pswriter.write("%d setlinejoin\n" % linejoin)
            if store:
                self.linejoin = linejoin

    def set_linecap(self, linecap, store=True):
        if linecap != self.linecap:
            self._pswriter.write("%d setlinecap\n" % linecap)
            if store:
                self.linecap = linecap

    def set_linedash(self, offset, seq, store=True):
        if self.linedash is not None:
            oldo, oldseq = self.linedash
            if np.array_equal(seq, oldseq) and oldo == offset:
                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=True):
        if mpl.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
                self.fontsize = fontsize

    def create_hatch(self, hatch):
        sidelen = 72
        if hatch in self._hatches:
            return self._hatches[hatch]
        name = 'H%d' % len(self._hatches)
        linewidth = mpl.rcParams['hatch.linewidth']
        pageheight = self.height * 72
        self._pswriter.write(f"""\
  << /PatternType 1
     /PaintType 2
     /TilingType 2
     /BBox[0 0 {sidelen:d} {sidelen:d}]
     /XStep {sidelen:d}
     /YStep {sidelen:d}

     /PaintProc {{
        pop
        {linewidth:f} setlinewidth
{self._convert_path(
    Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)}
        gsave
        fill
        grestore
        stroke
     }} bind
   >>
   matrix
   0.0 {pageheight:f} translate
   makepattern
   /{name} exch def
""")
        self._hatches[hatch] = name
        return name

    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, gc, x, y, im, transform=None):
        # docstring inherited

        h, w = im.shape[:2]
        imagecmd = "false 3 colorimage"
        data = im[::-1, :, :3]  # Vertically flipped rgb values.
        # data.tobytes().hex() has no spaces, so can be linewrapped by simply
        # splitting data every nchars. It's equivalent to textwrap.fill only
        # much faster.
        nchars = 128
        data = data.tobytes().hex()
        hexlines = "\n".join([
            data[n * nchars:(n + 1) * nchars]
            for n in range(math.ceil(len(data) / nchars))
        ])

        if transform is None:
            matrix = "1 0 0 1 0 0"
            xscale = w / self.image_magnification
            yscale = h / self.image_magnification
        else:
            matrix = " ".join(map(str, transform.frozen().to_values()))
            xscale = 1.0
            yscale = 1.0

        bbox = gc.get_clip_rectangle()
        clippath, clippath_trans = gc.get_clip_path()

        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)

        self._pswriter.write(f"""\
gsave
{clip}
{x:f} {y:f} translate
[{matrix}] concat
{xscale:f} {yscale:f} scale
/DataString {w:d} string def
{w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ]
{{
currentfile DataString readhexstring pop
}} bind {imagecmd}
{hexlines}
grestore
""")

    def _convert_path(self, path, transform, clip=False, simplify=None):
        if clip:
            clip = (0.0, 0.0, self.width * 72.0, self.height * 72.0)
        else:
            clip = None
        return _path.convert_to_string(path, transform, clip, simplify, None,
                                       6, [b'm', b'l', b'', b'c', b'cl'],
                                       True).decode('ascii')

    def _get_clip_path(self, clippath, clippath_transform):
        key = (clippath, id(clippath_transform))
        pid = self._clip_paths.get(key)
        if pid is None:
            pid = 'c%x' % len(self._clip_paths)
            clippath_bytes = self._convert_path(clippath,
                                                clippath_transform,
                                                simplify=False)
            self._pswriter.write(f"""\
/{pid} {{
{clippath_bytes}
clip
newpath
}} bind def
""")
            self._clip_paths[key] = pid
        return pid

    def draw_path(self, gc, path, transform, rgbFace=None):
        # docstring inherited
        clip = rgbFace is None and gc.get_hatch_path() is None
        simplify = path.should_simplify and clip
        ps = self._convert_path(path, transform, clip=clip, simplify=simplify)
        self._draw_ps(ps, gc, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        # docstring inherited

        if debugPS:
            self._pswriter.write('% draw_markers \n')

        ps_color = (None if _is_transparent(rgbFace) else '%1.3f setgray' %
                    rgbFace[0] if rgbFace[0] == rgbFace[1] == rgbFace[2] else
                    '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace[:3])

        # construct the generic marker command:

        # don't want the translate to be global
        ps_cmd = ['/o {', 'gsave', 'newpath', 'translate']

        lw = gc.get_linewidth()
        alpha = (gc.get_alpha() if gc.get_forced_alpha()
                 or len(gc.get_rgb()) == 3 else gc.get_rgb()[3])
        stroke = lw > 0 and alpha > 0
        if stroke:
            ps_cmd.append('%.1f setlinewidth' % lw)
            jint = gc.get_joinstyle()
            ps_cmd.append('%d setlinejoin' % jint)
            cint = gc.get_capstyle()
            ps_cmd.append('%d setlinecap' % cint)

        ps_cmd.append(
            self._convert_path(marker_path, marker_trans, simplify=False))

        if rgbFace:
            if stroke:
                ps_cmd.append('gsave')
            if ps_color:
                ps_cmd.extend([ps_color, 'fill'])
            if stroke:
                ps_cmd.append('grestore')

        if stroke:
            ps_cmd.append('stroke')
        ps_cmd.extend(['grestore', '} bind def'])

        for vertices, code in path.iter_segments(trans,
                                                 clip=(0, 0, self.width * 72,
                                                       self.height * 72),
                                                 simplify=False):
            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, 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 + 2) * uses_per_path
        # cost of definition+use is
        #     (len_path + 3) + 3 * 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 + 3 * uses_per_path + 3 < (len_path + 2) * 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)

        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)
            path_bytes = self._convert_path(path, transform, simplify=False)
            write(f"""\
/{name} {{
newpath
translate
{path_bytes}
}} bind def
""")
            path_codes.append(name)

        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):
            ps = "%g %g %s" % (xo, yo, path_id)
            self._draw_ps(ps, gc0, rgbFace)

        self._path_collection_id += 1

    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        # docstring inherited

        w, h, bl = self.get_text_width_height_descent(s, prop, ismath)
        fontsize = prop.get_size_in_points()
        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(mpl.rcParams['font.family'][0], r'{\rmfamily %s}')
        s = fontcmd % s
        tex = r'\color[rgb]{%s} %s' % (color, s)

        corr = 0  # w/2*(fontsize-10)/10
        if mpl.rcParams['text.latex.preview']:
            # use baseline alignment!
            pos = _nums_to_str(x - corr, y)
            self.psfrag.append(
                r'\psfrag{%s}[Bl][Bl][1][%f]{\fontsize{%f}{%f}%s}' %
                (thetext, angle, fontsize, fontsize * 1.25, tex))
        else:
            # Stick to the bottom alignment.
            pos = _nums_to_str(x - corr, y - bl)
            self.psfrag.append(
                r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' %
                (thetext, angle, fontsize, fontsize * 1.25, tex))

        self._pswriter.write(f"""\
gsave
{pos} moveto
({thetext})
show
grestore
""")
        self.textcnt += 1

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # docstring inherited

        # local to avoid repeated attribute lookups
        write = self._pswriter.write
        if debugPS:
            write("% text\n")

        if _is_transparent(gc.get_rgb()):
            return  # Special handling for fully transparent.

        if ismath == 'TeX':
            return self.draw_tex(gc, x, y, s, prop, angle)

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

        elif mpl.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), f"uni{ord(c):04X}")
                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)
            self._pswriter.write(f"""\
gsave
/{fontname} findfont
{fontsize} scalefont
setfont
{x:f} {y:f} translate
{angle:f} rotate
{thetext}
grestore
""")

        else:
            font = self._get_font_ttf(prop)
            font.set_text(s, 0, flags=LOAD_NO_HINTING)
            self._character_tracker.track(font, s)

            self.set_color(*gc.get_rgb())
            ps_name = (font.postscript_name.encode('ascii',
                                                   'replace').decode('ascii'))
            self.set_font(ps_name, prop.get_size_in_points())

            thetext = '\n'.join(
                '%f 0 m /%s glyphshow' % (x, font.get_glyph_name(glyph_idx))
                for glyph_idx, x in _text_layout.layout(s, font))
            self._pswriter.write(f"""\
gsave
{x:f} {y:f} translate
{angle:f} rotate
{thetext}
grestore
""")

    def new_gc(self):
        # docstring inherited
        return GraphicsContextPS()

    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._character_tracker.merge(used_characters)
        self.set_color(*gc.get_rgb())
        thetext = pswriter.getvalue()
        self._pswriter.write(f"""\
gsave
{x:f} {y:f} translate
{angle:f} rotate
{thetext}
grestore
""")

    def draw_gouraud_triangle(self, gc, points, colors, trans):
        self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)),
                                    colors.reshape((1, 3, 4)), trans)

    def draw_gouraud_triangles(self, gc, points, colors, trans):
        assert len(points) == len(colors)
        assert points.ndim == 3
        assert points.shape[1] == 3
        assert points.shape[2] == 2
        assert colors.ndim == 3
        assert colors.shape[1] == 3
        assert colors.shape[2] == 4

        shape = points.shape
        flat_points = points.reshape((shape[0] * shape[1], 2))
        flat_points = trans.transform(flat_points)
        flat_colors = colors.reshape((shape[0] * shape[1], 4))
        points_min = np.min(flat_points, axis=0) - (1 << 12)
        points_max = np.max(flat_points, axis=0) + (1 << 12)
        factor = np.ceil((2**32 - 1) / (points_max - points_min))

        xmin, ymin = points_min
        xmax, ymax = points_max

        streamarr = np.empty(shape[0] * shape[1],
                             dtype=[('flags', 'u1'), ('points', '2>u4'),
                                    ('colors', '3u1')])
        streamarr['flags'] = 0
        streamarr['points'] = (flat_points - points_min) * factor
        streamarr['colors'] = flat_colors[:, :3] * 255.0
        stream = quote_ps_string(streamarr.tobytes())

        self._pswriter.write(f"""\
gsave
<< /ShadingType 4
   /ColorSpace [/DeviceRGB]
   /BitsPerCoordinate 32
   /BitsPerComponent 8
   /BitsPerFlag 8
   /AntiAlias true
   /Decode [ {xmin:f} {xmax:f} {ymin:f} {ymax:f} 0 1 0 1 0 1 ]
   /DataSource ({stream})
>>
shfill
grestore
""")

    def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None):
        """
        Emit the PostScript snippet '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
                       and not _is_transparent(gc.get_rgb()))
        if not mightstroke:
            stroke = False
        if _is_transparent(rgbFace):
            fill = False
        hatch = gc.get_hatch()

        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 or hatch:
                write("gsave\n")
            self.set_color(*rgbFace[:3], store=False)
            write("fill\n")
            if stroke or hatch:
                write("grestore\n")

        if hatch:
            hatch_name = self.create_hatch(hatch)
            write("gsave\n")
            write("%f %f %f " % gc.get_hatch_color()[:3])
            write("%s setpattern fill grestore\n" % hatch_name)

        if stroke:
            write("stroke\n")

        write("grestore\n")
예제 #8
0
class TextToPath(object):
    """A class that converts strings to paths."""

    FONT_SCALE = 100.
    DPI = 72

    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self._texmanager = None

    @property
    @cbook.deprecated("3.0")
    def tex_font_map(self):
        return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))

    def _get_font(self, prop):
        """
        Find the `FT2Font` matching font properties *prop*, with its size set.
        """
        fname = font_manager.findfont(prop)
        font = get_font(fname)
        font.set_size(self.FONT_SCALE, self.DPI)
        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode))

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode))
        return char_id

    @cbook.deprecated(
        "3.1",
        alternative="font.get_path() and manual translation of the vertices")
    def glyph_to_path(self, font, currx=0.):
        """Convert the *font*'s current glyph to a (vertices, codes) pair."""
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = fontsize / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        Convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        Parameters
        ----------

        prop : `matplotlib.font_manager.FontProperties` instance
            The font properties for the text.

        s : str
            The text to be converted.

        usetex : bool, optional
            Whether to use tex rendering. Defaults to ``False``.

        ismath : bool, optional
            If True, use mathtext parser. Effective only if
            ``usetex == False``.

        Returns
        -------

        verts, codes : tuple of lists
            *verts*  is a list of numpy arrays containing the x and y
            coordinates of the vertices. *codes* is a list of path codes.

        Examples
        --------

        Create a list of vertices and codes from a text, and create a `Path`
        from those::

            from matplotlib.path import Path
            from matplotlib.textpath import TextToPath
            from matplotlib.font_manager import FontProperties

            fp = FontProperties(family="Humor Sans", style="italic")
            verts, codes = TextToPath().get_text_path(fp, "ABC")
            path = Path(verts, codes, closed=False)

        Also see `TextPath` for a more direct way to create a path from a text.
        """
        if not usetex:
            if not ismath:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(
                    font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
                    prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self,
                             font,
                             s,
                             glyph_map=None,
                             return_new_glyphs_only=False):
        """
        Convert string *s* to vertices and codes using the provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:
            ccode = ord(c)
            gind = font.get_char_index(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = glyph.linearHoriAdvance / 65536

            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                glyph_map_new[char_id] = font.get_path()

            currx += kern / 64

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, rects)

    def get_glyphs_mathtext(self,
                            prop,
                            s,
                            glyph_map=None,
                            return_new_glyphs_only=False):
        """
        Parse mathtext string *s* and convert it to a (vertices, codes) pair.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = font.get_path()

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, myrects)

    def get_texmanager(self):
        """Return the cached `~.texmanager.TexManager` instance."""
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self,
                       prop,
                       s,
                       glyph_map=None,
                       return_new_glyphs_only=False):
        """
        Process string *s* with usetex and convert it to a (vertices, codes)
        pair.
        """

        # Implementation mostly borrowed from pdf backend.

        dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
        with dviread.Dvi(dvifile, self.DPI) as dvi:
            page, = dvi

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        for x1, y1, dvifont, glyph, width in page.text:
            font, enc = self._get_ps_font_and_encoding(dvifont.texname)
            char_id = self._get_char_id_ps(font, glyph)

            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                if enc:
                    charcode = enc.get(glyph, None)
                else:
                    charcode = glyph

                ft2font_flag = LOAD_TARGET_LIGHT
                if charcode is not None:
                    glyph0 = font.load_char(charcode, flags=ft2font_flag)
                else:
                    _log.warning(
                        "The glyph (%d) of font (%s) cannot be "
                        "converted with the encoding. Glyph may "
                        "be wrong.", glyph, font.fname)

                    glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = font.get_path()

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, myrects)

    @staticmethod
    @functools.lru_cache(50)
    def _get_ps_font_and_encoding(texname):
        tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
        font_bunch = tex_font_map[texname]
        if font_bunch.filename is None:
            raise ValueError(("No usable font file found for %s (%s). "
                              "The font may lack a Type-1 version.") %
                             (font_bunch.psname, texname))

        font = get_font(font_bunch.filename)

        for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
                                           ("ADOBE_STANDARD", 1094995778)]:
            try:
                font.select_charmap(charmap_code)
            except (ValueError, RuntimeError):
                pass
            else:
                break
        else:
            charmap_name = ""
            _log.warning("No supported encoding in font (%s).",
                         font_bunch.filename)

        if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
            enc0 = dviread.Encoding(font_bunch.encoding)
            enc = {
                i: _get_adobe_standard_encoding().get(c, None)
                for i, c in enumerate(enc0.encoding)
            }
        else:
            enc = {}

        return font, enc
예제 #9
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """

    # 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 time and so the font cache is used by only one
    # renderer at a time.

    lock = threading.RLock()

    def __init__(self, width, height, dpi):
        RendererBase.__init__(self)

        self.dpi = dpi
        self.width = width
        self.height = height
        self._renderer = _RendererAgg(int(width), int(height), dpi)
        self._filter_renderers = []

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

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)

    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 _update_methods(self):
        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.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.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):
        # docstring inherited
        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 / 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)
                try:
                    self._renderer.draw_path(gc, p, transform, rgbFace)
                except OverflowError:
                    raise OverflowError("Exceeded cell block limit (set "
                                        "'agg.path.chunksize' rcparam)")
        else:
            try:
                self._renderer.draw_path(gc, path, transform, rgbFace)
            except OverflowError:
                raise OverflowError("Exceeded cell block limit (set "
                                    "'agg.path.chunksize' rcparam)")

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """
        Draw the math text using matplotlib.mathtext
        """
        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 = np.round(x + ox + xd)
        y = np.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):
        # docstring inherited

        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))

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

    def get_text_width_height_descent(self, s, prop, ismath):
        # docstring inherited

        if ismath in ["TeX", "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

        flags = get_hinting_flag()
        font = self._get_agg_font(prop)
        font.set_text(s, 0.0, flags=flags)
        w, h = font.get_width_height()  # width and height of unrotated string
        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):
        # docstring inherited
        # 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 = np.round(x + xd)
        y = np.round(y + yd)

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

    def get_canvas_width_height(self):
        # docstring inherited
        return self.width, self.height

    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, caching for efficiency
        """
        fname = findfont(prop)
        font = get_font(fname)

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

        return font

    def points_to_pixels(self, points):
        # docstring inherited
        return points * self.dpi / 72

    def tostring_rgb(self):
        return self._renderer.tostring_rgb()

    def tostring_argb(self):
        return self._renderer.tostring_argb()

    def buffer_rgba(self):
        return self._renderer.buffer_rgba()

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

    def option_image_nocomposite(self):
        # docstring inherited

        # 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):
        # docstring inherited
        return False

    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.
        """

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

        buffer, (l, b, w, h) = self.tostring_rgba_minimized()

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

        if w > 0 and h > 0:
            img = np.frombuffer(buffer, np.uint8)
            img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
                                          self.dpi)
            gc = self.new_gc()
            if img.dtype.kind == 'f':
                img = np.asarray(img * 255., np.uint8)
            img = img[::-1]
            self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
예제 #10
0
class RendererHTMLCanvas(RendererBase):
    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")

    def new_gc(self):
        return GraphicsContextHTMLCanvas(renderer=self)

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

    def _matplotlib_color_to_CSS(self,
                                 color,
                                 alpha,
                                 alpha_overrides,
                                 is_RGB=True):
        if not is_RGB:
            R, G, B, alpha = colorConverter.to_rgba(color)
            color = (R, G, B)

        if (len(color) == 4) and (alpha is None):
            alpha = color[3]

        if alpha is None:
            CSS_color = rgb2hex(color[:3])

        else:
            R = int(color[0] * 255)
            G = int(color[1] * 255)
            B = int(color[2] * 255)
            if len(color) == 3 or alpha_overrides:
                CSS_color = f"""rgba({R:d}, {G:d}, {B:d}, {alpha:.3g})"""
            else:
                CSS_color = """rgba({:d}, {:d}, {:d}, {:.3g})""".format(
                    R, G, B, color[3])

        return CSS_color

    def _set_style(self, gc, rgbFace=None):
        if rgbFace is not None:
            self.ctx.fillStyle = self._matplotlib_color_to_CSS(
                rgbFace, gc.get_alpha(), gc.get_forced_alpha())

        if gc.get_capstyle():
            self.ctx.lineCap = _capstyle_d[gc.get_capstyle()]

        self.ctx.strokeStyle = self._matplotlib_color_to_CSS(
            gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha())

        self.ctx.lineWidth = self.points_to_pixels(gc.get_linewidth())

    def _path_helper(self, ctx, path, transform, clip=None):
        ctx.beginPath()
        for points, code in path.iter_segments(transform,
                                               remove_nans=True,
                                               clip=clip):
            if code == Path.MOVETO:
                ctx.moveTo(points[0], points[1])
            elif code == Path.LINETO:
                ctx.lineTo(points[0], points[1])
            elif code == Path.CURVE3:
                ctx.quadraticCurveTo(*points)
            elif code == Path.CURVE4:
                ctx.bezierCurveTo(*points)
            elif code == Path.CLOSEPOLY:
                ctx.closePath()

    def draw_path(self, gc, path, transform, rgbFace=None):
        self._set_style(gc, rgbFace)
        if rgbFace is None and gc.get_hatch() is None:
            figure_clip = (0, 0, self.width, self.height)

        else:
            figure_clip = None

        transform += Affine2D().scale(1, -1).translate(0, self.height)
        self._path_helper(self.ctx, path, transform, figure_clip)

        if rgbFace is not None:
            self.ctx.fill()
            self.ctx.fillStyle = "#000000"

        if gc.stroke:
            self.ctx.stroke()

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

    def draw_image(self, gc, x, y, im, transform=None):
        im = np.flipud(im)
        h, w, d = im.shape
        y = self.ctx.height - y - h
        im = np.ravel(np.uint8(np.reshape(im, (h * w * d, -1)))).tobytes()
        pixels_proxy = create_proxy(im)
        pixels_buf = pixels_proxy.getBuffer("u8clamped")
        img_data = ImageData.new(pixels_buf.data, w, h)
        self.ctx.save()
        in_memory_canvas = document.createElement("canvas")
        in_memory_canvas.width = w
        in_memory_canvas.height = h
        in_memory_canvas_context = in_memory_canvas.getContext("2d")
        in_memory_canvas_context.putImageData(img_data, 0, 0)
        self.ctx.drawImage(in_memory_canvas, x, y, w, h)
        self.ctx.restore()
        pixels_proxy.destroy()
        pixels_buf.release()

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

    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
            h /= 64.0
            d = font.get_descent() / 64.0
        return w, h, d

    def _draw_math_text(self, gc, x, y, s, prop, angle):
        rgba, descent = self.mathtext_parser.to_rgba(s, gc.get_rgb(), self.dpi,
                                                     prop.get_size_in_points())
        height, width, _ = rgba.shape
        angle = math.radians(angle)
        if angle != 0:
            self.ctx.save()
            self.ctx.translate(x, y)
            self.ctx.rotate(-angle)
            self.ctx.translate(-x, -y)
        self.draw_image(gc, x, -y - descent, np.flipud(rgba))
        if angle != 0:
            self.ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        def _load_font_into_web(loaded_face):
            document.fonts.add(loaded_face)
            window.font_counter += 1
            self.fig.draw_idle()

        if ismath:
            self._draw_math_text(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 - self.ctx.height
        font_size = self.points_to_pixels(prop.get_size_in_points())

        _, font_file_name = self._get_font(prop)

        font_face_arguments = (
            prop.get_name(),
            f"url({_base_fonts_url + font_file_name})",
        )

        # The following snippet loads a font into the browser's
        # environment if it wasn't loaded before. This check is necessary
        # to help us avoid loading the same font multiple times. Further,
        # it helps us to avoid the infinite loop of
        # load font --> redraw --> load font --> redraw --> ....

        if font_face_arguments not in _font_set:
            _font_set.add(font_face_arguments)
            f = FontFace.new(*font_face_arguments)
            f.load().then(_load_font_into_web)

        font_property_string = "{} {} {:.3g}px {}, {}".format(
            prop.get_style(),
            prop.get_weight(),
            font_size,
            prop.get_name(),
            prop.get_family()[0],
        )
        if angle != 0:
            self.ctx.save()
            self.ctx.translate(x, y)
            self.ctx.rotate(-angle)
            self.ctx.translate(-x, -y)
        self.ctx.font = font_property_string
        self.ctx.fillStyle = self._matplotlib_color_to_CSS(
            gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha())
        self.ctx.fillText(s, x, y)
        self.ctx.fillStyle = "#000000"
        if angle != 0:
            self.ctx.restore()
예제 #11
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.hatch = None
        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 set_hatch(self, hatch):
        """
        hatch can be one of:
            /   - diagonal hatching
            \   - back diagonal
            |   - vertical
            -   - horizontal
            +   - crossed
            X   - crossed diagonal

        letters can be combined, in which case all the specified
        hatchings are done

        if same letter repeats, it increases the density of hatching
        in that direction
        """
        hatches = {'horiz':0, 'vert':0, 'diag1':0, 'diag2':0}

        for letter in hatch:
            if   (letter == '/'):    hatches['diag2'] += 1
            elif (letter == '\\'):   hatches['diag1'] += 1
            elif (letter == '|'):    hatches['vert']  += 1
            elif (letter == '-'):    hatches['horiz'] += 1
            elif (letter == '+'):
                hatches['horiz'] += 1
                hatches['vert'] += 1
            elif (letter.lower() == 'x'):
                hatches['diag1'] += 1
                hatches['diag2'] += 1

        def do_hatch(angle, density):
            if (density == 0): return ""
            return """\
  gsave
   eoclip %s rotate 0.0 0.0 0.0 0.0 setrgbcolor 0 setlinewidth
   /hatchgap %d def
   pathbbox /hatchb exch def /hatchr exch def /hatcht exch def /hatchl exch def
   hatchl cvi hatchgap idiv hatchgap mul
   hatchgap
   hatchr cvi hatchgap idiv hatchgap mul
   {hatcht m 0 hatchb hatcht sub r }
   for
   stroke
  grestore
 """ % (angle, 12/density)
        self._pswriter.write("gsave\n")
        self._pswriter.write(do_hatch(90, hatches['horiz']))
        self._pswriter.write(do_hatch(0, hatches['vert']))
        self._pswriter.write(do_hatch(45, hatches['diag1']))
        self._pswriter.write(do_hatch(-45, hatches['diag2']))
        self._pswriter.write("grestore\n")

    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()
            l,b,r,t = texmanager.get_ps_bbox(s, fontsize)
            w = (r-l)
            h = (t-b)
            # TODO: We need a way to get a good baseline from
            # text.usetex
            return w, h, 0

        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\ngrestore\n")
            else:
                self.set_color(store=0, *rgbFace[:3])
                write("fill\n")

        hatch = gc.get_hatch()
        if hatch:
            self.set_hatch(hatch)

        if stroke:
            write("stroke\n")

        write("grestore\n")
예제 #12
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 = {}
        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(
            "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 six.PY3:
            content = content.encode("utf8")
        return "%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(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("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": "1.0",
                        "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"] = str(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"] = str(rgbFace[3])

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

        offset, seq = gc.get_dashes()
        if seq is not None:
            attrib["stroke-dasharray"] = ",".join(["%f" % val for val in seq])
            attrib["stroke-dashoffset"] = six.text_type(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"] = str(rgb[3])
            if linewidth != 1.0:
                attrib["stroke-width"] = str(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=six.text_type(x), y=six.text_type(y), width=six.text_type(w), height=six.text_type(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 = FT2Font(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(six.text_type(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": six.text_type(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):
        """
        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["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"] = six.text_type(x)
                attrib["y"] = six.text_type(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,
    ):
        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": six.text_type(xo),
                "y": six.text_type(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=six.text_type(x1),
                y1=six.text_type(y1),
                x2=six.text_type(xb),
                y2=six.text_type(yb),
            )
            writer.element(
                "stop", offset="0", style=generate_css({"stop-color": rgb2hex(c), "stop-opacity": six.text_type(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([six.text_type(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": str(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, 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("g", attrib={"clip-path": "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["transform"] = generate_transform([("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()

        if dx is None:
            w = 72.0 * w / self.image_dpi
        else:
            w = dx

        if dy is None:
            h = 72.0 * h / self.image_dpi
        else:
            h = dy

        oid = getattr(im, "_gid", None)
        url = getattr(im, "_url", None)
        if url is not None:
            self.writer.start("a", attrib={"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"] = "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)
            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["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(
                "image",
                x=six.text_type(x / trans[0]),
                y=six.text_type((self.height - y) / trans[3] - h),
                width=six.text_type(w),
                height=six.text_type(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["transform"] = generate_transform([("matrix", flipped)])
            self.writer.element(
                "image",
                x=six.text_type(x),
                y=six.text_type(y),
                width=six.text_type(dx),
                height=six.text_type(abs(dy)),
                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"] = six.text_type(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"] = six.text_type(xposition)
                if yposition != 0.0:
                    attrib["y"] = six.text_type(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"] = six.text_type(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"] = six.text_type(fontsize) + "px"
            style["font-family"] = six.text_type(fontfamily)
            style["font-style"] = prop.get_style().lower()
            attrib["style"] = generate_css(style)

            if 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.0
                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"] = str(ax)
                attrib["y"] = str(ay)
                attrib["style"] = generate_css(style)
                attrib["transform"] = "rotate(%f, %f, %f)" % (-angle, ax, 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 = {}
            for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                style = generate_css(
                    {
                        "font-size": six.text_type(fontsize) + "px",
                        "font-family": font.family_name,
                        "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 list(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(six.text_type(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=six.text_type(x),
                        y=six.text_type(-y + height),
                        width=six.text_type(width),
                        height=six.text_type(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)
예제 #13
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }
    def __init__(self, dpi):
        """
        """
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.dpi = dpi
        self.gc = GraphicsContextCairo (renderer=self)
        self.text_ctx = cairo.Context (
           cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1))
        self.mathtext_parser = MathTextParser('Cairo')
    def set_ctx_from_surface (self, surface):
        self.gc.ctx = cairo.Context (surface)
    def set_width_height(self, width, height):
        self.width  = width
        self.height = height
        self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height)
    def _fill_and_stroke (self, ctx, fill_c, alpha):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha*fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()
    @staticmethod
    def convert_path(ctx, path, transform):
        for points, code in path.iter_segments(transform):
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1],
                             points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
    def draw_path(self, gc, path, transform, rgbFace=None):
        if len(path.vertices) > 18980:
           raise ValueError("The Cairo backend can not draw paths longer than 18980 points.")
        ctx = gc.ctx
        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)
        ctx.new_path()
        self.convert_path(ctx, path, transform)
        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha())
    def draw_image(self, gc, x, y, im):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        clippath, clippath_trans = gc.get_clip_path()
        im.flipud_out()
        rows, cols, buf = im.color_conv (BYTE_FORMAT)
        surface = cairo.ImageSurface.create_for_data (
                      buf, cairo.FORMAT_ARGB32, cols, rows, cols*4)
        ctx = self.gc.ctx
        ctx.save()
        if clippath is not None:
            ctx.new_path()
            RendererCairo.convert_path(ctx, clippath, clippath_trans)
            ctx.clip()
        y = self.height - y - rows
        ctx.set_source_surface (surface, x, y)
        ctx.paint()
        ctx.restore()
        im.flipud_out()
    def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        if ismath:
           self._draw_mathtext(gc, x, y, s, prop, angle)
        else:
           ctx = gc.ctx
           ctx.new_path()
           ctx.move_to (x, y)
           ctx.select_font_face (prop.get_name(),
                                 self.fontangles [prop.get_style()],
                                 self.fontweights[prop.get_weight()])
           size = prop.get_size_in_points() * self.dpi / 72.0
           ctx.save()
           if angle:
              ctx.rotate (-angle * npy.pi / 180)
           ctx.set_font_size (size)
           ctx.show_text (s.encode("utf-8"))
           ctx.restore()
    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)
        ctx.save()
        ctx.translate(x, y)
        if angle:
           ctx.rotate (-angle * npy.pi / 180)
        for font, fontsize, s, ox, oy in glyphs:
           ctx.new_path()
           ctx.move_to(ox, oy)
           fontProp = ttfFontProperty(font)
           ctx.save()
           ctx.select_font_face (fontProp.name,
                                 self.fontangles [fontProp.style],
                                 self.fontweights[fontProp.weight])
           size = fontsize * self.dpi / 72.0
           ctx.set_font_size(size)
           ctx.show_text(s.encode("utf-8"))
           ctx.restore()
        for ox, oy, w, h in rects:
           ctx.new_path()
           ctx.rectangle (ox, oy, w, h)
           ctx.set_source_rgb (0, 0, 0)
           ctx.fill_preserve()
        ctx.restore()
    def flipy(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return True
    def get_canvas_width_height(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return self.width, self.height
    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
               s, self.dpi, prop)
            return width, height, descent
        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],
                              self.fontweights[prop.get_weight()])
        size = prop.get_size_in_points() * self.dpi / 72.0
        ctx.set_font_size (size)
        y_bearing, w, h = ctx.text_extents (s)[1:4]
        ctx.restore()
        return w, h, h + y_bearing
    def new_gc(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.gc.ctx.save()
        return self.gc
    def points_to_pixels(self, points):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return points/72.0 * self.dpi
예제 #14
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 = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        if gc.get_url() is not None:
            self._svgwriter.write('<a xlink:href="%s">' % gc.get_url())
        style = self._get_style(gc, rgbFace)
        self._svgwriter.write('<%s style="%s" %s %s/>\n' %
                              (element, style, clippath, details))
        if gc.get_url() is not None:
            self._svgwriter.write('</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(str(dictkey)).hexdigest()
            self._svgwriter.write('<defs>\n  <pattern id="%s" ' % id)
            self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ')
            self._svgwriter.write(' 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(
                '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' %
                (HATCH_SIZE + 1, HATCH_SIZE + 1, fill))
            path = '<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('\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 = "url(#%s)" % self._get_hatch(gc, rgbFace)
        else:
            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, 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 = '<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)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % 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, 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('<g id="%s">\n' % (gid))
        else:
            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%f %f',
        Path.LINETO: 'L%f %f',
        Path.CURVE3: 'Q%f %f %f %f',
        Path.CURVE4: '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 = '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)
        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('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),
                                 simplify=False)
        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)
        for vertices, code in path.iter_segments(trans_and_flip,
                                                 simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                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, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls):
        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, simplify=False)
            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, 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('<a xlink:href="%s">' % url)
            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(gc0, rgbFace)
            self._svgwriter.write('<use style="%s" %s/>\n' % (style, details))
            if clipid is not None:
                write('</g>')
            if url is not None:
                self._svgwriter.write('</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('<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(
                '<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">'
                % (self._n_gradients, i, x1, y1, xb, yb))
            write('<stop offset="0" style="stop-color:%s;stop-opacity:%f"/>' %
                  (rgb2hex(c), c[-1]))
            write('<stop offset="1" style="stop-color:%s;stop-opacity:0"/>' %
                  rgb2hex(c))
            write('</linearGradient>')

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

        avg_color = np.sum(colors[:, :], axis=0) / 3.0
        write('<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(
                '<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 = ''
        else:
            clippath = 'clip-path="url(#%s)"' % clipid

        write('<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('</g>\n')

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

        trans = [1, 0, 0, 1, 0, 0]
        transstr = ''
        if rcParams['svg.image_noscale']:
            trans = list(im.get_matrix())
            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()

        url = getattr(im, '_url', None)
        if url is not None:
            self._svgwriter.write('<a xlink:href="%s">' % url)
        self._svgwriter.write(
            '<image x="%f" y="%f" width="%f" height="%f" '
            '%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("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')
        if url is not None:
            self._svgwriter.write('</a>')

    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):
        """
        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('<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 = '<path id="%s" d="%s"/>\n' % (
                        char_id, ''.join(path_data))
                    write(path_element)
                write('</defs>\n')

                glyph_map.update(glyph_map_new)

            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: %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 / text2path.FONT_SCALE))

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

            svg.append('</g>\n')
            if clipid is not None:
                svg.append('</g>\n')
            svg = ''.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('<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 = '<path id="%s" d="%s"/>\n' % (
                        char_id, ''.join(path_data))
                    write(path_element)
                write('</defs>\n')

                glyph_map.update(glyph_map_new)

            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: %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,-%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('<use xlink:href="#%s"' % char_id)
                svg.append(' x="%f" y="%f" transform="scale(%f)"' %
                           (xposition / scale, yposition / scale, scale))
                svg.append('/>\n')

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

            svg.append('</g><!-- style -->\n')
            if clipid is not None:
                svg.append('</g><!-- clipid -->\n')
            svg = ''.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, 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: %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
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' x="%f"' % (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: %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(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 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 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
예제 #15
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):
        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):
        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
예제 #16
0
class TextToPath(object):
    """
    A class that convert a given text to a path using ttf fonts.
    """

    FONT_SCALE = 100.
    DPI = 72

    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

        self._adobe_standard_encoding = None

    def _get_adobe_standard_encoding(self):
        enc_name = dviread.find_tex_file('8a.enc')
        enc = dviread.Encoding(enc_name)
        return dict([(c, i) for i, c in enumerate(enc.encoding)])

    def _get_font(self, prop):
        """
        find a ttf font.
        """
        fname = font_manager.findfont(prop)
        font = FT2Font(str(fname))
        font.set_size(self.FONT_SCALE, self.DPI)

        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%x' % (ps_name, ccode))
        return char_id

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.quote('%s-%d' % (ps_name, ccode))
        return char_id

    def glyph_to_path(self, font, currx=0.):
        """
        convert the ft2font glyph to vertices and codes.
        """
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = float(fontsize) / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. Effective only if usetex == False.


        """
        if not usetex:
            if not ismath:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(
                                                    font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
                                                    prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self, font, s, glyph_map=None,
                             return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using the
        provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        cmap = font.get_charmap()
        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:
            ccode = ord(c)
            gind = cmap.get(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = (glyph.linearHoriAdvance / 65536.0)

            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                glyph_map_new[char_id] = self.glyph_to_path(font)

            currx += (kern / 64.0)

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (zip(glyph_ids, xpositions, ypositions, sizes),
                glyph_map_new, rects)

    def get_glyphs_mathtext(self, prop, s, glyph_map=None,
                            return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes by parsing it with
        mathtext.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        currx, curry = 0, 0
        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = self.glyph_to_path(font)

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
                     (ox + w, oy), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (zip(glyph_ids, xpositions, ypositions, sizes),
                glyph_map_new, myrects)

    def get_texmanager(self):
        """
        return the :class:`matplotlib.texmanager.TexManager` instance
        """
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self, prop, s, glyph_map=None,
                       return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using matplotlib's usetex
        mode.
        """

        # codes are modstly borrowed from pdf backend.

        texmanager = self.get_texmanager()

        if self.tex_font_map is None:
            self.tex_font_map = dviread.PsfontsMap(
                                    dviread.find_tex_file('pdftex.map'))

        if self._adobe_standard_encoding is None:
            self._adobe_standard_encoding = self._get_adobe_standard_encoding()

        fontsize = prop.get_size_in_points()
        if hasattr(texmanager, "get_dvi"):
            dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
            dvi = dviread.DviFromFileLike(dvifilelike, self.DPI)
        else:
            dvifile = texmanager.make_dvi(s, self.FONT_SCALE)
            dvi = dviread.Dvi(dvifile, self.DPI)
        try:
            page = next(iter(dvi))
        finally:
            dvi.close()

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        # oldfont, seq = None, []
        for x1, y1, dvifont, glyph, width in page.text:
            font_and_encoding = self._ps_fontd.get(dvifont.texname)
            font_bunch = self.tex_font_map[dvifont.texname]

            if font_and_encoding is None:
                font = FT2Font(str(font_bunch.filename))

                for charmap_name, charmap_code in [("ADOBE_CUSTOM",
                                                    1094992451),
                                                   ("ADOBE_STANDARD",
                                                    1094995778)]:
                    try:
                        font.select_charmap(charmap_code)
                    except ValueError:
                        pass
                    else:
                        break
                else:
                    charmap_name = ""
                    warnings.warn("No supported encoding in font (%s)." %
                                  font_bunch.filename)

                if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
                    enc0 = dviread.Encoding(font_bunch.encoding)
                    enc = dict([(i, self._adobe_standard_encoding.get(c, None))
                                for i, c in enumerate(enc0.encoding)])
                else:
                    enc = dict()
                self._ps_fontd[dvifont.texname] = font, enc

            else:
                font, enc = font_and_encoding

            ft2font_flag = LOAD_TARGET_LIGHT

            char_id = self._get_char_id_ps(font, glyph)

            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                if enc:
                    charcode = enc.get(glyph, None)
                else:
                    charcode = glyph

                if charcode is not None:
                    glyph0 = font.load_char(charcode, flags=ft2font_flag)
                else:
                    warnings.warn("The glyph (%d) of font (%s) cannot be "
                                  "converted with the encoding. Glyph may "
                                  "be wrong" % (glyph, font_bunch.filename))

                    glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = self.glyph_to_path(font)

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
                     (ox, oy + h), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (zip(glyph_ids, xpositions, ypositions, sizes),
                glyph_map_new, myrects)
예제 #17
0
class TextToPath(object):
    """A class that converts strings to paths."""

    FONT_SCALE = 100.
    DPI = 72

    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self._texmanager = None

    @property
    @cbook.deprecated("3.0")
    def tex_font_map(self):
        return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))

    def _get_font(self, prop):
        """
        Find the `FT2Font` matching font properties *prop*, with its size set.
        """
        fname = font_manager.findfont(prop)
        font = get_font(fname)
        font.set_size(self.FONT_SCALE, self.DPI)
        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode))

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode))
        return char_id

    @cbook.deprecated(
        "3.1",
        alternative="font.get_path() and manual translation of the vertices")
    def glyph_to_path(self, font, currx=0.):
        """Convert the *font*'s current glyph to a (vertices, codes) pair."""
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = fontsize / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        Convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        Parameters
        ----------

        prop : `matplotlib.font_manager.FontProperties` instance
            The font properties for the text.

        s : str
            The text to be converted.

        usetex : bool, optional
            Whether to use tex rendering. Defaults to ``False``.

        ismath : bool, optional
            If True, use mathtext parser. Effective only if
            ``usetex == False``.

        Returns
        -------

        verts, codes : tuple of lists
            *verts*  is a list of numpy arrays containing the x and y
            coordinates of the vertices. *codes* is a list of path codes.

        Examples
        --------

        Create a list of vertices and codes from a text, and create a `Path`
        from those::

            from matplotlib.path import Path
            from matplotlib.textpath import TextToPath
            from matplotlib.font_manager import FontProperties

            fp = FontProperties(family="Humor Sans", style="italic")
            verts, codes = TextToPath().get_text_path(fp, "ABC")
            path = Path(verts, codes, closed=False)

        Also see `TextPath` for a more direct way to create a path from a text.
        """
        if not usetex:
            if not ismath:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(
                                                    font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
                                                    prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self, font, s, glyph_map=None,
                             return_new_glyphs_only=False):
        """
        Convert string *s* to vertices and codes using the provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:
            ccode = ord(c)
            gind = font.get_char_index(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = glyph.linearHoriAdvance / 65536

            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                glyph_map_new[char_id] = font.get_path()

            currx += kern / 64

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, rects)

    def get_glyphs_mathtext(self, prop, s, glyph_map=None,
                            return_new_glyphs_only=False):
        """
        Parse mathtext string *s* and convert it to a (vertices, codes) pair.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = font.get_path()

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
                     (ox + w, oy), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)

    def get_texmanager(self):
        """Return the cached `~.texmanager.TexManager` instance."""
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self, prop, s, glyph_map=None,
                       return_new_glyphs_only=False):
        """
        Process string *s* with usetex and convert it to a (vertices, codes)
        pair.
        """

        # Implementation mostly borrowed from pdf backend.

        dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
        with dviread.Dvi(dvifile, self.DPI) as dvi:
            page, = dvi

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        for x1, y1, dvifont, glyph, width in page.text:
            font, enc = self._get_ps_font_and_encoding(dvifont.texname)
            char_id = self._get_char_id_ps(font, glyph)

            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                if enc:
                    charcode = enc.get(glyph, None)
                else:
                    charcode = glyph

                ft2font_flag = LOAD_TARGET_LIGHT
                if charcode is not None:
                    glyph0 = font.load_char(charcode, flags=ft2font_flag)
                else:
                    _log.warning("The glyph (%d) of font (%s) cannot be "
                                 "converted with the encoding. Glyph may "
                                 "be wrong.", glyph, font.fname)

                    glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = font.get_path()

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
                     (ox, oy + h), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)

    @staticmethod
    @functools.lru_cache(50)
    def _get_ps_font_and_encoding(texname):
        tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
        font_bunch = tex_font_map[texname]
        if font_bunch.filename is None:
            raise ValueError(
                ("No usable font file found for %s (%s). "
                    "The font may lack a Type-1 version.")
                % (font_bunch.psname, texname))

        font = get_font(font_bunch.filename)

        for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
                                           ("ADOBE_STANDARD", 1094995778)]:
            try:
                font.select_charmap(charmap_code)
            except (ValueError, RuntimeError):
                pass
            else:
                break
        else:
            charmap_name = ""
            _log.warning("No supported encoding in font (%s).",
                         font_bunch.filename)

        if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
            enc0 = dviread.Encoding(font_bunch.encoding)
            enc = {i: _get_adobe_standard_encoding().get(c, None)
                   for i, c in enumerate(enc0.encoding)}
        else:
            enc = {}

        return font, enc
예제 #18
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """
    debug = 1

    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')

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

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

    def _update_methods(self):
        #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_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.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized

    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)

        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)

        flags = self._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()

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

        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 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 = self._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):
        # 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 = np.array(Z * 255.0, np.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

    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

            self._renderer.restore_region2(region, x1, y1, x2, y2, ox, 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.
        """

        from matplotlib._image import fromarray

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

        buffer, bounds = self._renderer.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)
            image.flipud_out()

            gc = self.new_gc()
            self._renderer.draw_image(gc, l + ox, height - b - h + oy, image)
예제 #19
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """

    # 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 time and so the font cache is used by only one
    # renderer at a time.

    lock = threading.RLock()

    def __init__(self, width, height, dpi):
        super().__init__()

        self.dpi = dpi
        self.width = width
        self.height = height
        self._renderer = _RendererAgg(int(width), int(height), dpi)
        self._filter_renderers = []

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

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)

    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 _update_methods(self):
        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.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.copy_from_bbox = self._renderer.copy_from_bbox

    def draw_path(self, gc, path, transform, rgbFace=None):
        # docstring inherited
        nmax = mpl.rcParams['agg.path.chunksize']  # here at least for testing
        npts = path.vertices.shape[0]

        if (npts > nmax > 100 and path.should_simplify and rgbFace is None
                and gc.get_hatch() is None):
            nch = np.ceil(npts / 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)
                p.simplify_threshold = path.simplify_threshold
                try:
                    self._renderer.draw_path(gc, p, transform, rgbFace)
                except OverflowError:
                    msg = (
                        "Exceeded cell block limit in Agg.\n\n"
                        "Please reduce the value of "
                        f"rcParams['agg.path.chunksize'] (currently {nmax}) "
                        "or increase the path simplification threshold"
                        "(rcParams['path.simplify_threshold'] = "
                        f"{mpl.rcParams['path.simplify_threshold']:.2f} by "
                        "default and path.simplify_threshold = "
                        f"{path.simplify_threshold:.2f} on the input).")
                    raise OverflowError(msg) from None
        else:
            try:
                self._renderer.draw_path(gc, path, transform, rgbFace)
            except OverflowError:
                cant_chunk = ''
                if rgbFace is not None:
                    cant_chunk += "- can not split filled path\n"
                if gc.get_hatch() is not None:
                    cant_chunk += "- can not split hatched path\n"
                if not path.should_simplify:
                    cant_chunk += "- path.should_simplify is False\n"
                if len(cant_chunk):
                    msg = (
                        "Exceeded cell block limit in Agg, however for the "
                        "following reasons:\n\n"
                        f"{cant_chunk}\n"
                        "we can not automatically split up this path to draw."
                        "\n\nPlease manually simplify your path.")

                else:
                    inc_threshold = (
                        "or increase the path simplification threshold"
                        "(rcParams['path.simplify_threshold'] = "
                        f"{mpl.rcParams['path.simplify_threshold']} "
                        "by default and path.simplify_threshold "
                        f"= {path.simplify_threshold} "
                        "on the input).")
                    if nmax > 100:
                        msg = (
                            "Exceeded cell block limit in Agg.  Please reduce "
                            "the value of rcParams['agg.path.chunksize'] "
                            f"(currently {nmax}) {inc_threshold}")
                    else:
                        msg = ("Exceeded cell block limit in Agg.  Please set "
                               "the value of rcParams['agg.path.chunksize'], "
                               f"(currently {nmax}) to be greater than 100 " +
                               inc_threshold)

                raise OverflowError(msg) from None

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """Draw mathtext using :mod:`matplotlib.mathtext`."""
        ox, oy, width, height, descent, font_image = \
            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):
        # docstring inherited
        if ismath:
            return self.draw_mathtext(gc, x, y, s, prop, angle)
        font = self._prepare_font(prop)
        # 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=get_hinting_flag())
        font.draw_glyphs_to_bitmap(
            antialiased=mpl.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))
        x = round(x + xo + xd)
        y = round(y + yo + yd)
        self._renderer.draw_text_image(font, x, y + 1, angle, gc)

    def get_text_width_height_descent(self, s, prop, ismath):
        # docstring inherited

        _api.check_in_list(["TeX", True, False], ismath=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, font_image = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        font = self._prepare_font(prop)
        font.set_text(s, 0.0, flags=get_hinting_flag())
        w, h = font.get_width_height()  # width and height of unrotated string
        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, *, mtext=None):
        # docstring inherited
        # 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="TeX")
        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):
        # docstring inherited
        return self.width, self.height

    def _prepare_font(self, font_prop):
        """
        Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size.
        """
        font = get_font(findfont(font_prop))
        font.clear()
        size = font_prop.get_size_in_points()
        font.set_size(size, self.dpi)
        return font

    def points_to_pixels(self, points):
        # docstring inherited
        return points * self.dpi / 72

    def buffer_rgba(self):
        return memoryview(self._renderer)

    def tostring_argb(self):
        return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()

    def tostring_rgb(self):
        return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()

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

    def option_image_nocomposite(self):
        # docstring inherited

        # 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):
        # docstring inherited
        return False

    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 pair of floats) 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.
        """
        orig_img = np.asarray(self.buffer_rgba())
        slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3])
        cropped_img = orig_img[slice_y, slice_x]

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

        if cropped_img.size:
            img, ox, oy = post_processing(cropped_img / 255, self.dpi)
            gc = self.new_gc()
            if img.dtype.kind == 'f':
                img = np.asarray(img * 255., np.uint8)
            self._renderer.draw_image(gc, slice_x.start + ox,
                                      int(self.height) - slice_y.stop + oy,
                                      img[::-1])
예제 #20
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """
    debug=1
    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_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_gouraud_triangle = self._renderer.draw_gouraud_triangle
        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')
    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:
            font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT)
        font.draw_glyphs_to_bitmap()
        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
        """
        if ismath=='TeX':
            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):
        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):
        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
        specify the new position (of the LLC of the originally region,
        not the LLC of the bbox) that 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
            self._renderer.restore_region2(region, x1, y1, x2, y2, ox, oy)
        else:
            self._renderer.restore_region(region)
예제 #21
0
class RendererCairo(RendererBase):
    @cbook.deprecated("3.3")
    @property
    def fontweights(self):
        return {
            100: cairo.FONT_WEIGHT_NORMAL,
            200: cairo.FONT_WEIGHT_NORMAL,
            300: cairo.FONT_WEIGHT_NORMAL,
            400: cairo.FONT_WEIGHT_NORMAL,
            500: cairo.FONT_WEIGHT_NORMAL,
            600: cairo.FONT_WEIGHT_BOLD,
            700: cairo.FONT_WEIGHT_BOLD,
            800: cairo.FONT_WEIGHT_BOLD,
            900: cairo.FONT_WEIGHT_BOLD,
            'ultralight': cairo.FONT_WEIGHT_NORMAL,
            'light': cairo.FONT_WEIGHT_NORMAL,
            'normal': cairo.FONT_WEIGHT_NORMAL,
            'medium': cairo.FONT_WEIGHT_NORMAL,
            'regular': cairo.FONT_WEIGHT_NORMAL,
            'semibold': cairo.FONT_WEIGHT_BOLD,
            'bold': cairo.FONT_WEIGHT_BOLD,
            'heavy': cairo.FONT_WEIGHT_BOLD,
            'ultrabold': cairo.FONT_WEIGHT_BOLD,
            'black': cairo.FONT_WEIGHT_BOLD,
        }

    @cbook.deprecated("3.3")
    @property
    def fontangles(self):
        return {
            'italic': cairo.FONT_SLANT_ITALIC,
            'normal': cairo.FONT_SLANT_NORMAL,
            'oblique': cairo.FONT_SLANT_OBLIQUE,
        }

    def __init__(self, dpi):
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
            cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        self.mathtext_parser = MathTextParser('Cairo')
        RendererBase.__init__(self)

    def set_ctx_from_surface(self, surface):
        self.gc.ctx = cairo.Context(surface)
        # Although it may appear natural to automatically call
        # `self.set_width_height(surface.get_width(), surface.get_height())`
        # here (instead of having the caller do so separately), this would fail
        # for PDF/PS/SVG surfaces, which have no way to report their extents.

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

    def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    def draw_path(self, gc, path, transform, rgbFace=None):
        # docstring inherited
        ctx = gc.ctx
        # Clip the path to the actual rendering extents if it isn't filled.
        clip = (ctx.clip_extents()
                if rgbFace is None and gc.get_hatch() is None else None)
        transform = (transform +
                     Affine2D().scale(1, -1).translate(0, self.height))
        ctx.new_path()
        _append_path(ctx, path, transform, clip)
        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                              gc.get_forced_alpha())

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     transform,
                     rgbFace=None):
        # docstring inherited

        ctx = gc.ctx
        ctx.new_path()
        # Create the path for the marker; it needs to be flipped here already!
        _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
        marker_path = ctx.copy_path_flat()

        # Figure out whether the path has a fill
        x1, y1, x2, y2 = ctx.fill_extents()
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            filled = False
            # No fill, just unset this (so we don't try to fill it later on)
            rgbFace = None
        else:
            filled = True

        transform = (transform +
                     Affine2D().scale(1, -1).translate(0, self.height))

        ctx.new_path()
        for i, (vertices, codes) in enumerate(
                path.iter_segments(transform, simplify=False)):
            if len(vertices):
                x, y = vertices[-2:]
                ctx.save()

                # Translate and apply path
                ctx.translate(x, y)
                ctx.append_path(marker_path)

                ctx.restore()

                # Slower code path if there is a fill; we need to draw
                # the fill and stroke for each marker at the same time.
                # Also flush out the drawing every once in a while to
                # prevent the paths from getting way too long.
                if filled or i % 1000 == 0:
                    self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                                          gc.get_forced_alpha())

        # Fast path, if there is no fill, draw everything in one step
        if not filled:
            self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                                  gc.get_forced_alpha())

    def draw_image(self, gc, x, y, im):
        im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
        surface = cairo.ImageSurface.create_for_data(im.ravel().data,
                                                     cairo.FORMAT_ARGB32,
                                                     im.shape[1], im.shape[0],
                                                     im.shape[1] * 4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # docstring inherited

        # Note: (x, y) are device/display coords, not user-coords, unlike other
        # draw_* methods
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to(x, y)

            ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
            ctx.save()
            ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72)
            if angle:
                ctx.rotate(np.deg2rad(-angle))
            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate(np.deg2rad(-angle))

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            ctx.select_font_face(
                *_cairo_font_args_from_font_prop(ttfFontProperty(font)))
            ctx.set_font_size(fontsize * self.dpi / 72)
            ctx.show_text(s)

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle(ox, oy, w, h)
            ctx.set_source_rgb(0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()

    def get_canvas_width_height(self):
        # docstring inherited
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        # docstring inherited

        if ismath == 'TeX':
            return super().get_text_width_height_descent(s, prop, ismath)

        if ismath:
            dims = self.mathtext_parser.parse(s, self.dpi, prop)
            return dims[0:3]  # return width, height, descent

        ctx = self.text_ctx
        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.save()
        ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small
        ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72)

        y_bearing, w, h = ctx.text_extents(s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing

    def new_gc(self):
        # docstring inherited
        self.gc.ctx.save()
        self.gc._alpha = 1
        self.gc._forced_alpha = False  # if True, _alpha overrides A from RGBA
        return self.gc

    def points_to_pixels(self, points):
        # docstring inherited
        return points / 72 * self.dpi
예제 #22
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        """
        """
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.dpi = dpi
        self.text_ctx = cairo.Context (
           cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1))
        self.mathtext_parser = MathTextParser('Cairo')

    def set_ctx_from_surface (self, surface):
        self.ctx = cairo.Context (surface)
        self.ctx.save() # restore, save  - when call new_gc()


    def set_width_height(self, width, height):
        self.width  = width
        self.height = height
        self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height)
        # use matrix_flipy for ALL rendering?
        # - problem with text? - will need to switch matrix_flipy off, or do a
        # font transform?


    def _fill_and_stroke (self, ctx, fill_c, alpha):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha*fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()


    #@staticmethod
    def convert_path(ctx, tpath):
        for points, code in tpath.iter_segments():
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1],
                             points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
    convert_path = staticmethod(convert_path)


    def draw_path(self, gc, path, transform, rgbFace=None):
        if len(path.vertices) > 18980:
           raise ValueError("The Cairo backend can not draw paths longer than 18980 points.")

        ctx = gc.ctx
        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)
        tpath = transform.transform_path(path)

        ctx.new_path()
        self.convert_path(ctx, tpath)

        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha())

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        # bbox - not currently used
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        im.flipud_out()

        rows, cols, buf = im.color_conv (BYTE_FORMAT)
        surface = cairo.ImageSurface.create_for_data (
                      buf, cairo.FORMAT_ARGB32, cols, rows, cols*4)
        # function does not pass a 'gc' so use renderer.ctx
        ctx = self.ctx
        y = self.height - y - rows
        ctx.set_source_surface (surface, x, y)
        ctx.paint()

        im.flipud_out()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

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

        else:
           ctx = gc.ctx
           ctx.new_path()
           ctx.move_to (x, y)
           ctx.select_font_face (prop.get_name(),
                                 self.fontangles [prop.get_style()],
                                 self.fontweights[prop.get_weight()])

           size = prop.get_size_in_points() * self.dpi / 72.0

           ctx.save()
           if angle:
              ctx.rotate (-angle * npy.pi / 180)
           ctx.set_font_size (size)
           ctx.show_text (s.encode("utf-8"))
           ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
           ctx.rotate (-angle * npy.pi / 180)

        for font, fontsize, s, ox, oy in glyphs:
           ctx.new_path()
           ctx.move_to(ox, oy)

           fontProp = ttfFontProperty(font)
           ctx.save()
           ctx.select_font_face (fontProp.name,
                                 self.fontangles [fontProp.style],
                                 self.fontweights[fontProp.weight])

           size = fontsize * self.dpi / 72.0
           ctx.set_font_size(size)
           ctx.show_text(s.encode("utf-8"))
           ctx.restore()

        for ox, oy, w, h in rects:
           ctx.new_path()
           ctx.rectangle (ox, oy, w, h)
           ctx.set_source_rgb (0, 0, 0)
           ctx.fill_preserve()

        ctx.restore()


    def flipy(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return True
        #return False # tried - all draw objects ok except text (and images?)
        # which comes out mirrored!


    def get_canvas_width_height(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return self.width, self.height


    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
               s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],
                              self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small

        size = prop.get_size_in_points() * self.dpi / 72.0

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size (size)

        y_bearing, w, h = ctx.text_extents (s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing


    def new_gc(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.ctx.restore()  # matches save() in set_ctx_from_surface()
        self.ctx.save()
        return GraphicsContextCairo (renderer=self)


    def points_to_pixels(self, points):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return points/72.0 * self.dpi
예제 #23
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
예제 #24
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):
        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:
            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()
            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()

    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
예제 #25
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
예제 #26
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")
예제 #27
0
파일: backend_ps.py 프로젝트: runt18/nupic
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.hatch = None
        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("{0:1.3f} setgray\n".format(r))
            else:
                self._pswriter.write("{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor\n".format(r, g, b))
            if store: self.color = (r,g,b)

    def set_linewidth(self, linewidth, store=1):
        if linewidth != self.linewidth:
            self._pswriter.write("{0:1.3f} setlinewidth\n".format(linewidth))
            if store: self.linewidth = linewidth

    def set_linejoin(self, linejoin, store=1):
        if linejoin != self.linejoin:
            self._pswriter.write("{0:d} setlinejoin\n".format(linejoin))
            if store: self.linejoin = linejoin

    def set_linecap(self, linecap, store=1):
        if linecap != self.linecap:
            self._pswriter.write("{0:d} setlinecap\n".format(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="[{0!s}] {1:d} setdash\n".format(_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 set_hatch(self, hatch):
        """
        hatch can be one of:
            /   - diagonal hatching
            \   - back diagonal
            |   - vertical
            -   - horizontal
            +   - crossed
            X   - crossed diagonal

        letters can be combined, in which case all the specified
        hatchings are done

        if same letter repeats, it increases the density of hatching
        in that direction
        """
        hatches = {'horiz':0, 'vert':0, 'diag1':0, 'diag2':0}

        for letter in hatch:
            if   (letter == '/'):    hatches['diag2'] += 1
            elif (letter == '\\'):   hatches['diag1'] += 1
            elif (letter == '|'):    hatches['vert']  += 1
            elif (letter == '-'):    hatches['horiz'] += 1
            elif (letter == '+'):
                hatches['horiz'] += 1
                hatches['vert'] += 1
            elif (letter.lower() == 'x'):
                hatches['diag1'] += 1
                hatches['diag2'] += 1

        def do_hatch(angle, density):
            if (density == 0): return ""
            return """\
  gsave
   eoclip {0!s} rotate 0.0 0.0 0.0 0.0 setrgbcolor 0 setlinewidth
   /hatchgap {1:d} def
   pathbbox /hatchb exch def /hatchr exch def /hatcht exch def /hatchl exch def
   hatchl cvi hatchgap idiv hatchgap mul
   hatchgap
   hatchr cvi hatchgap idiv hatchgap mul
   {{hatcht m 0 hatchb hatcht sub r }}
   for
   stroke
  grestore
 """.format(angle, 12/density)
        self._pswriter.write("gsave\n")
        self._pswriter.write(do_hatch(90, hatches['horiz']))
        self._pswriter.write(do_hatch(0, hatches['vert']))
        self._pswriter.write(do_hatch(45, hatches['diag1']))
        self._pswriter.write(do_hatch(-45, hatches['diag2']))
        self._pswriter.write("grestore\n")

    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()
            l,b,r,t = texmanager.get_ps_bbox(s, fontsize)
            w = (r-l)
            h = (t-b)
            # TODO: We need a way to get a good baseline from
            # text.usetex
            return w, h, 0

        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('{0!s} clipbox'.format(_nums_to_str(clipw, cliph, clipx, clipy)))
        if clippath is not None:
            id = self._get_clip_path(clippath, clippath_trans)
            clip.append('{0!s}'.format(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
""".format(**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("{0:g} {1:g} m".format(*tuple(points)))
            elif code == Path.LINETO:
                ps.append("{0:g} {1:g} l".format(*tuple(points)))
            elif code == Path.CURVE3:
                points = quad2cubic(*(list(last_points[-2:]) + list(points)))
                ps.append("{0:g} {1:g} {2:g} {3:g} {4:g} {5:g} c".format(*
                          tuple(points[2:])))
            elif code == Path.CURVE4:
                ps.append("{0:g} {1:g} {2:g} {3:g} {4:g} {5:g} c".format(*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{0:x}'.format(len(self._clip_paths))
            ps_cmd = ['/{0!s} {{'.format(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 = '{0:1.3f} setgray'.format(rgbFace[0])
            else:
                ps_color = '{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor'.format(*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("{0:g} {1:g} o".format(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{0:x}_{1:x}'.format(self._path_collection_id, i)
            ps_cmd = ['/{0!s} {{'.format(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 = "{0:g} {1:g} {2!s}".format(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{0:d}'.format(self.textcnt)
        color = '{0:1.3f},{1:1.3f},{2:1.3f}'.format(*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]{{{0!s}}} {1!s}'.format(color, s)
        self.psfrag.append(r'\psfrag{{{0!s}}}[bl][bl][1][{1:f}]{{\fontsize{{{2:f}}}{{{3:f}}}{4!s}}}'.format(thetext, angle, fontsize, fontsize*1.25, tex))
        ps = """\
gsave
{pos!s} moveto
({thetext!s})
show
grestore
    """.format(**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 = '({0!s})'.format(s)
            fontname = font.get_fontname()
            fontsize = prop.get_size_in_points()
            rotate = '{0:1.1f} rotate'.format(angle)
            setcolor = '{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor'.format(*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
    """.format(**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("{0!s} m\n".format(_nums_to_str(x,y)))
            if angle:
                write("gsave\n")
                write("{0!s} rotate\n".format(_num_to_str(angle)))
            descent = font.get_descent() / 64.0
            if descent:
                write("0 {0!s} rmoveto\n".format(_num_to_str(descent)))
            write("({0!s}) show\n".format(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('{0:f} {1:f} m /{2!s} glyphshow'.format(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
    """.format(**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('{0:f} {1:f} m /{2!s} glyphshow'.format(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
""".format(**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
""".format(**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('{0:1.4g} {1:1.4g} {2:1.4g} {3:1.4g} clipbox\n'.format(w, h, x, y))
        clippath, clippath_trans = gc.get_clip_path()
        if clippath:
            id = self._get_clip_path(clippath, clippath_trans)
            write('{0!s}\n'.format(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\ngrestore\n")
            else:
                self.set_color(store=0, *rgbFace[:3])
                write("fill\n")

        hatch = gc.get_hatch()
        if hatch:
            self.set_hatch(hatch)

        if stroke:
            write("stroke\n")

        write("grestore\n")
예제 #28
0
class RendererGDK(RendererBase):
    fontweights = {
        100          : pango.WEIGHT_ULTRALIGHT,
        200          : pango.WEIGHT_LIGHT,
        300          : pango.WEIGHT_LIGHT,
        400          : pango.WEIGHT_NORMAL,
        500          : pango.WEIGHT_NORMAL,
        600          : pango.WEIGHT_BOLD,
        700          : pango.WEIGHT_BOLD,
        800          : pango.WEIGHT_HEAVY,
        900          : pango.WEIGHT_ULTRABOLD,
        'ultralight' : pango.WEIGHT_ULTRALIGHT,
        'light'      : pango.WEIGHT_LIGHT,
        'normal'     : pango.WEIGHT_NORMAL,
        'medium'     : pango.WEIGHT_NORMAL,
        'semibold'   : pango.WEIGHT_BOLD,
        'bold'       : pango.WEIGHT_BOLD,
        'heavy'      : pango.WEIGHT_HEAVY,
        'ultrabold'  : pango.WEIGHT_ULTRABOLD,
        'black'      : pango.WEIGHT_ULTRABOLD,
                   }

    # cache for efficiency, these must be at class, not instance level
    layoutd = {}  # a map from text prop tups to pango layouts
    rotated = {}  # a map from text prop tups to rotated text pixbufs

    def __init__(self, gtkDA, dpi):
        # widget gtkDA is used for:
        #  '<widget>.create_pango_layout(s)'
        #  cmap line below)
        self.gtkDA = gtkDA
        self.dpi   = dpi
        self._cmap = gtkDA.get_colormap()
        self.mathtext_parser = MathTextParser("Agg")

    def set_pixmap (self, pixmap):
        self.gdkDrawable = pixmap

    def set_width_height (self, width, height):
        """w,h is the figure w,h not the pixmap w,h
        """
        self.width, self.height = width, height

    def draw_path(self, gc, path, transform, rgbFace=None):
        transform = transform + Affine2D(). \
            scale(1.0, -1.0).translate(0, self.height)
        polygons = path.to_polygons(transform, self.width, self.height)
        for polygon in polygons:
            # draw_polygon won't take an arbitrary sequence -- it must be a list
            # of tuples
            polygon = [(int(round(x)), int(round(y))) for x, y in polygon]
            if rgbFace is not None:
                saveColor = gc.gdkGC.foreground
                gc.gdkGC.foreground = gc.rgb_to_gdk_color(rgbFace)
                self.gdkDrawable.draw_polygon(gc.gdkGC, True, polygon)
                gc.gdkGC.foreground = saveColor
            if gc.gdkGC.line_width > 0:
                self.gdkDrawable.draw_lines(gc.gdkGC, polygon)

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        if bbox != None:
            l,b,w,h = bbox.bounds
            #rectangle = (int(l), self.height-int(b+h),
            #             int(w), int(h))
            # set clip rect?

        im.flipud_out()
        rows, cols, image_str = im.as_rgba_str()

        image_array = npy.fromstring(image_str, npy.uint8)
        image_array.shape = rows, cols, 4

        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
                                has_alpha=True, bits_per_sample=8,
                                width=cols, height=rows)

        array = pixbuf_get_pixels_array(pixbuf)
        array[:,:,:] = image_array

        gc = self.new_gc()


        y = self.height-y-rows

        try: # new in 2.2
            # can use None instead of gc.gdkGC, if don't need clipping
            self.gdkDrawable.draw_pixbuf (gc.gdkGC, pixbuf, 0, 0,
                                          int(x), int(y), cols, rows,
                                          gdk.RGB_DITHER_NONE, 0, 0)
        except AttributeError:
            # deprecated in 2.2
            pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0,
                                  int(x), int(y), cols, rows,
                                  gdk.RGB_DITHER_NONE, 0, 0)

        # unflip
        im.flipud_out()


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

        if x <0 or y <0: # window has shrunk and text is off the edge
            return

        if angle not in (0,90):
            warnings.warn('backend_gdk: unable to draw text at angles ' +
                          'other than 0 or 90')
        elif ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        elif angle==90:
            self._draw_rotated_text(gc, x, y, s, prop, angle)

        else:
            layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
            l, b, w, h = inkRect
            self.gdkDrawable.draw_layout(gc.gdkGC, x, y-h-b, layout)


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

        if angle==90:
            width, height = height, width
            x -= width
        y -= height

        imw = font_image.get_width()
        imh = font_image.get_height()
        N = imw * imh

        # a numpixels by num fonts array
        Xall = npy.zeros((N,1), npy.uint8)

        image_str = font_image.as_str()
        Xall[:,0] = npy.fromstring(image_str, npy.uint8)

        # get the max alpha at each pixel
        Xs = npy.amax(Xall,axis=1)

        # convert it to it's proper shape
        Xs.shape = imh, imw

        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=True,
                                bits_per_sample=8, width=imw, height=imh)

        array = pixbuf_get_pixels_array(pixbuf)

        rgb = gc.get_rgb()
        array[:,:,0]=int(rgb[0]*255)
        array[:,:,1]=int(rgb[1]*255)
        array[:,:,2]=int(rgb[2]*255)
        array[:,:,3]=Xs

        try: # new in 2.2
            # can use None instead of gc.gdkGC, if don't need clipping
            self.gdkDrawable.draw_pixbuf (gc.gdkGC, pixbuf, 0, 0,
                                          int(x), int(y), imw, imh,
                                          gdk.RGB_DITHER_NONE, 0, 0)
        except AttributeError:
            # deprecated in 2.2
            pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0,
                                  int(x), int(y), imw, imh,
                                  gdk.RGB_DITHER_NONE, 0, 0)


    def _draw_rotated_text(self, gc, x, y, s, prop, angle):
        """
        Draw the text rotated 90 degrees, other angles are not supported
        """
        # this function (and its called functions) is a bottleneck
        # Pango 1.6 supports rotated text, but pygtk 2.4.0 does not yet have
        # wrapper functions
        # GTK+ 2.6 pixbufs support rotation

        gdrawable = self.gdkDrawable
        ggc = gc.gdkGC

        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
        l, b, w, h = inkRect
        x = int(x-h)
        y = int(y-w)

        if x < 0 or y < 0: # window has shrunk and text is off the edge
            return

        key = (x,y,s,angle,hash(prop))
        imageVert = self.rotated.get(key)
        if imageVert != None:
            gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
            return

        imageBack = gdrawable.get_image(x, y, w, h)
        imageVert = gdrawable.get_image(x, y, h, w)
        imageFlip = gtk.gdk.Image(type=gdk.IMAGE_FASTEST,
                                  visual=gdrawable.get_visual(),
                                  width=w, height=h)
        if imageFlip == None or imageBack == None or imageVert == None:
            warnings.warn("Could not renderer vertical text")
            return
        imageFlip.set_colormap(self._cmap)
        for i in range(w):
            for j in range(h):
                imageFlip.put_pixel(i, j, imageVert.get_pixel(j,w-i-1) )

        gdrawable.draw_image(ggc, imageFlip, 0, 0, x, y, w, h)
        gdrawable.draw_layout(ggc, x, y-b, layout)

        imageIn  = gdrawable.get_image(x, y, w, h)
        for i in range(w):
            for j in range(h):
                imageVert.put_pixel(j, i, imageIn.get_pixel(w-i-1,j) )

        gdrawable.draw_image(ggc, imageBack, 0, 0, x, y, w, h)
        gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
        self.rotated[key] = imageVert


    def _get_pango_layout(self, s, prop):
        """
        Create a pango layout instance for Text 's' with properties 'prop'.
        Return - pango layout (from cache if already exists)

        Note that pango assumes a logical DPI of 96
        Ref: pango/fonts.c/pango_font_description_set_size() manual page
        """
        # problem? - cache gets bigger and bigger, is never cleared out
        # two (not one) layouts are created for every text item s (then they
        # are cached) - why?

        key = self.dpi, s, hash(prop)
        value = self.layoutd.get(key)
        if value != None:
            return value

        size = prop.get_size_in_points() * self.dpi / 96.0
        size = round(size)

        font_str = '%s, %s %i' % (prop.get_name(), prop.get_style(), size,)
        font = pango.FontDescription(font_str)

        # later - add fontweight to font_str
        font.set_weight(self.fontweights[prop.get_weight()])

        layout = self.gtkDA.create_pango_layout(s)
        layout.set_font_description(font)
        inkRect, logicalRect = layout.get_pixel_extents()

        self.layoutd[key] = layout, inkRect, logicalRect
        return layout, inkRect, logicalRect


    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:
            ox, oy, width, height, descent, font_image, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
        l, b, w, h = inkRect
        return w, h+1, h + 1

    def new_gc(self):
        return GraphicsContextGDK(renderer=self)


    def points_to_pixels(self, points):
        return points/72.0 * self.dpi
예제 #29
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """

    debug = 1

    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")

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

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

    def _update_methods(self):
        # 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_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.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized

    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)

        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)

        flags = self._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()

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

        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 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 = self._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):
        # 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 = np.array(Z * 255.0, np.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

    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

            self._renderer.restore_region2(region, x1, y1, x2, y2, ox, 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.
        """

        from matplotlib._image import fromarray

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

        buffer, bounds = self._renderer.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.0, self.dpi)
            image = fromarray(img, 1)
            image.flipud_out()

            gc = self.new_gc()
            self._renderer.draw_image(gc, l + ox, height - b - h + oy, image)
예제 #30
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        """
        """
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.dpi = dpi
        self.gc = GraphicsContextCairo (renderer=self)
        self.text_ctx = cairo.Context (
           cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1))
        self.mathtext_parser = MathTextParser('Cairo')

        RendererBase.__init__(self)

    def set_ctx_from_surface (self, surface):
        self.gc.ctx = cairo.Context (surface)


    def set_width_height(self, width, height):
        self.width  = width
        self.height = height
        self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height)
        # use matrix_flipy for ALL rendering?
        # - problem with text? - will need to switch matrix_flipy off, or do a
        # font transform?


    def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    def convert_path(ctx, path, transform, clip=None):
        for points, code in path.iter_segments(transform, clip=clip):
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1],
                             points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)


    def draw_path(self, gc, path, transform, rgbFace=None):
        ctx = gc.ctx

        # We'll clip the path to the actual rendering extents
        # if the path isn't filled.
        if rgbFace is None and gc.get_hatch() is None:
            clip = ctx.clip_extents()
        else:
            clip = None

        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)

        ctx.new_path()
        self.convert_path(ctx, path, transform, clip)

        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None):
        ctx = gc.ctx

        ctx.new_path()
        # Create the path for the marker; it needs to be flipped here already!
        self.convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
        marker_path = ctx.copy_path_flat()

        # Figure out whether the path has a fill
        x1, y1, x2, y2 = ctx.fill_extents()
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            filled = False
            # No fill, just unset this (so we don't try to fill it later on)
            rgbFace = None
        else:
            filled = True

        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)

        ctx.new_path()
        for i, (vertices, codes) in enumerate(path.iter_segments(transform, simplify=False)):
            if len(vertices):
                x, y = vertices[-2:]
                ctx.save()

                # Translate and apply path
                ctx.translate(x, y)
                ctx.append_path(marker_path)

                ctx.restore()

                # Slower code path if there is a fill; we need to draw
                # the fill and stroke for each marker at the same time.
                # Also flush out the drawing every once in a while to
                # prevent the paths from getting way too long.
                if filled or i % 1000 == 0:
                    self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

        # Fast path, if there is no fill, draw everything in one step
        if not filled:
            self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_image(self, gc, x, y, im):
        # bbox - not currently used
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        if sys.byteorder == 'little':
            im = im[:, :, (2, 1, 0, 3)]
        else:
            im = im[:, :, (3, 0, 1, 2)]
        if HAS_CAIRO_CFFI:
            # cairocffi tries to use the buffer_info from array.array
            # that we replicate in ArrayWrapper and alternatively falls back
            # on ctypes to get a pointer to the numpy array. This works
            # correctly on a numpy array in python3 but not 2.7. We replicate
            # the array.array functionality here to get cross version support.
            imbuffer = ArrayWrapper(im.flatten())
        else:
            # py2cairo uses PyObject_AsWriteBuffer
            # to get a pointer to the numpy array this works correctly
            # on a regular numpy array but not on a memory view.
            # At the time of writing the latest release version of
            # py3cairo still does not support create_for_data
            imbuffer = im.flatten()
        surface = cairo.ImageSurface.create_for_data(imbuffer,
                                                     cairo.FORMAT_ARGB32,
                                                     im.shape[1],
                                                     im.shape[0],
                                                     im.shape[1]*4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        if gc.get_alpha() != 1.0:
            ctx.paint_with_alpha(gc.get_alpha())
        else:
            ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

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

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to (x, y)
            ctx.select_font_face (prop.get_name(),
                                  self.fontangles [prop.get_style()],
                                  self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate (-angle * np.pi / 180)
            ctx.set_font_size (size)

            if HAS_CAIRO_CFFI:
                if not isinstance(s, six.text_type):
                    s = six.text_type(s)
            else:
                if not six.PY3 and isinstance(s, six.text_type):
                    s = s.encode("utf-8")

            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate (-angle * np.pi / 180)

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.save()
            ctx.select_font_face (fontProp.name,
                                  self.fontangles [fontProp.style],
                                  self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            if not six.PY3 and isinstance(s, six.text_type):
                s = s.encode("utf-8")
            ctx.show_text(s)
            ctx.restore()

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle (ox, oy, w, h)
            ctx.set_source_rgb (0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()


    def flipy(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return True
        #return False # tried - all draw objects ok except text (and images?)
        # which comes out mirrored!


    def get_canvas_width_height(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return self.width, self.height


    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
               s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],
                              self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small

        size = prop.get_size_in_points() * self.dpi / 72.0

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size (size)

        y_bearing, w, h = ctx.text_extents (s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing


    def new_gc(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.gc.ctx.save()
        self.gc._alpha = 1.0
        self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
        return self.gc


    def points_to_pixels(self, points):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return points/72.0 * self.dpi
예제 #31
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.0, height / 2.0, 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()
class RendererCairo(RendererBase):
    fontweights = {
        100: cairo.FONT_WEIGHT_NORMAL,
        200: cairo.FONT_WEIGHT_NORMAL,
        300: cairo.FONT_WEIGHT_NORMAL,
        400: cairo.FONT_WEIGHT_NORMAL,
        500: cairo.FONT_WEIGHT_NORMAL,
        600: cairo.FONT_WEIGHT_BOLD,
        700: cairo.FONT_WEIGHT_BOLD,
        800: cairo.FONT_WEIGHT_BOLD,
        900: cairo.FONT_WEIGHT_BOLD,
        'ultralight': cairo.FONT_WEIGHT_NORMAL,
        'light': cairo.FONT_WEIGHT_NORMAL,
        'normal': cairo.FONT_WEIGHT_NORMAL,
        'medium': cairo.FONT_WEIGHT_NORMAL,
        'regular': cairo.FONT_WEIGHT_NORMAL,
        'semibold': cairo.FONT_WEIGHT_BOLD,
        'bold': cairo.FONT_WEIGHT_BOLD,
        'heavy': cairo.FONT_WEIGHT_BOLD,
        'ultrabold': cairo.FONT_WEIGHT_BOLD,
        'black': cairo.FONT_WEIGHT_BOLD,
    }
    fontangles = {
        'italic': cairo.FONT_SLANT_ITALIC,
        'normal': cairo.FONT_SLANT_NORMAL,
        'oblique': cairo.FONT_SLANT_OBLIQUE,
    }

    def __init__(self, dpi):
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
            cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        self.mathtext_parser = MathTextParser('Cairo')
        RendererBase.__init__(self)

    def set_ctx_from_surface(self, surface):
        self.gc.ctx = cairo.Context(surface)
        # Although it may appear natural to automatically call
        # `self.set_width_height(surface.get_width(), surface.get_height())`
        # here (instead of having the caller do so separately), this would fail
        # for PDF/PS/SVG surfaces, which have no way to report their extents.

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

    def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    @cbook.deprecated("3.0")
    def convert_path(ctx, path, transform, clip=None):
        _append_path(ctx, path, transform, clip)

    def draw_path(self, gc, path, transform, rgbFace=None):
        ctx = gc.ctx
        # Clip the path to the actual rendering extents if it isn't filled.
        clip = (ctx.clip_extents()
                if rgbFace is None and gc.get_hatch() is None else None)
        transform = (transform +
                     Affine2D().scale(1, -1).translate(0, self.height))
        ctx.new_path()
        _append_path(ctx, path, transform, clip)
        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                              gc.get_forced_alpha())

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     transform,
                     rgbFace=None):
        ctx = gc.ctx

        ctx.new_path()
        # Create the path for the marker; it needs to be flipped here already!
        _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
        marker_path = ctx.copy_path_flat()

        # Figure out whether the path has a fill
        x1, y1, x2, y2 = ctx.fill_extents()
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            filled = False
            # No fill, just unset this (so we don't try to fill it later on)
            rgbFace = None
        else:
            filled = True

        transform = (transform +
                     Affine2D().scale(1, -1).translate(0, self.height))

        ctx.new_path()
        for i, (vertices, codes) in enumerate(
                path.iter_segments(transform, simplify=False)):
            if len(vertices):
                x, y = vertices[-2:]
                ctx.save()

                # Translate and apply path
                ctx.translate(x, y)
                ctx.append_path(marker_path)

                ctx.restore()

                # Slower code path if there is a fill; we need to draw
                # the fill and stroke for each marker at the same time.
                # Also flush out the drawing every once in a while to
                # prevent the paths from getting way too long.
                if filled or i % 1000 == 0:
                    self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                                          gc.get_forced_alpha())

        # Fast path, if there is no fill, draw everything in one step
        if not filled:
            self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(),
                                  gc.get_forced_alpha())

    def draw_path_collection(self, gc, main_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):

        path_ids = []
        for path, transform in self._iter_collection_raw_paths(
                main_transform, paths, all_transforms):
            path_ids.append((path, Affine2D(transform)))

        reuse_key = None
        grouped_draw = []

        def _draw_paths():
            if not grouped_draw:
                return
            gc_vars, rgb_fc = reuse_key
            gc = copy.copy(gc0)
            # We actually need to call the setters to reset the internal state.
            vars(gc).update(gc_vars)
            for k, v in gc_vars.items():
                if k == "_linestyle":  # Deprecated, no effect.
                    continue
                try:
                    getattr(gc, "set" + k)(v)
                except (AttributeError, TypeError) as e:
                    pass
            gc.ctx.new_path()
            paths, transforms = zip(*grouped_draw)
            grouped_draw.clear()
            _append_paths(gc.ctx, paths, transforms)
            self._fill_and_stroke(gc.ctx, rgb_fc, gc.get_alpha(),
                                  gc.get_forced_alpha())

        for xo, yo, path_id, gc0, rgb_fc in self._iter_collection(
                gc, main_transform, all_transforms, path_ids, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds, urls, offset_position):
            path, transform = path_id
            transform = (Affine2D(transform.get_matrix()).translate(
                xo, yo - self.height).scale(1, -1))
            # rgb_fc could be a ndarray, for which equality is elementwise.
            new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None
            if new_key == reuse_key:
                grouped_draw.append((path, transform))
            else:
                _draw_paths()
                grouped_draw.append((path, transform))
                reuse_key = new_key
        _draw_paths()

    def draw_image(self, gc, x, y, im):
        im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
        surface = cairo.ImageSurface.create_for_data(im.ravel().data,
                                                     cairo.FORMAT_ARGB32,
                                                     im.shape[1], im.shape[0],
                                                     im.shape[1] * 4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to(x, y)
            ctx.select_font_face(prop.get_name(),
                                 self.fontangles[prop.get_style()],
                                 self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate(np.deg2rad(-angle))
            ctx.set_font_size(size)

            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate(np.deg2rad(-angle))

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.select_font_face(fontProp.name,
                                 self.fontangles[fontProp.style],
                                 self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            ctx.show_text(s)

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle(ox, oy, w, h)
            ctx.set_source_rgb(0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()

    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, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face(prop.get_name(),
                             self.fontangles[prop.get_style()],
                             self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small
        size = prop.get_size_in_points() * self.dpi / 72

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size(size)

        y_bearing, w, h = ctx.text_extents(s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing

    def new_gc(self):
        self.gc.ctx.save()
        self.gc._alpha = 1
        self.gc._forced_alpha = False  # if True, _alpha overrides A from RGBA
        return self.gc

    def points_to_pixels(self, points):
        return points / 72 * self.dpi
예제 #33
0
class RendererGDK(RendererBase):
    fontweights = {
        100: pango.WEIGHT_ULTRALIGHT,
        200: pango.WEIGHT_LIGHT,
        300: pango.WEIGHT_LIGHT,
        400: pango.WEIGHT_NORMAL,
        500: pango.WEIGHT_NORMAL,
        600: pango.WEIGHT_BOLD,
        700: pango.WEIGHT_BOLD,
        800: pango.WEIGHT_HEAVY,
        900: pango.WEIGHT_ULTRABOLD,
        'ultralight': pango.WEIGHT_ULTRALIGHT,
        'light': pango.WEIGHT_LIGHT,
        'normal': pango.WEIGHT_NORMAL,
        'medium': pango.WEIGHT_NORMAL,
        'semibold': pango.WEIGHT_BOLD,
        'bold': pango.WEIGHT_BOLD,
        'heavy': pango.WEIGHT_HEAVY,
        'ultrabold': pango.WEIGHT_ULTRABOLD,
        'black': pango.WEIGHT_ULTRABOLD,
    }

    # cache for efficiency, these must be at class, not instance level
    layoutd = {}  # a map from text prop tups to pango layouts
    rotated = {}  # a map from text prop tups to rotated text pixbufs

    def __init__(self, gtkDA, dpi):
        # widget gtkDA is used for:
        #  '<widget>.create_pango_layout(s)'
        #  cmap line below)
        self.gtkDA = gtkDA
        self.dpi = dpi
        self._cmap = gtkDA.get_colormap()
        self.mathtext_parser = MathTextParser("Agg")

    def set_pixmap(self, pixmap):
        self.gdkDrawable = pixmap

    def set_width_height(self, width, height):
        """w,h is the figure w,h not the pixmap w,h
        """
        self.width, self.height = width, height

    def draw_path(self, gc, path, transform, rgbFace=None):
        transform = transform + Affine2D(). \
            scale(1.0, -1.0).translate(0, self.height)
        polygons = path.to_polygons(transform, self.width, self.height)
        for polygon in polygons:
            # draw_polygon won't take an arbitrary sequence -- it must be a list
            # of tuples
            polygon = [(int(round(x)), int(round(y))) for x, y in polygon]
            if rgbFace is not None:
                saveColor = gc.gdkGC.foreground
                gc.gdkGC.foreground = gc.rgb_to_gdk_color(rgbFace)
                self.gdkDrawable.draw_polygon(gc.gdkGC, True, polygon)
                gc.gdkGC.foreground = saveColor
            if gc.gdkGC.line_width > 0:
                self.gdkDrawable.draw_lines(gc.gdkGC, polygon)

    def draw_image(self, gc, x, y, im):
        bbox = gc.get_clip_rectangle()

        if bbox != None:
            l, b, w, h = bbox.bounds
            #rectangle = (int(l), self.height-int(b+h),
            #             int(w), int(h))
            # set clip rect?

        im.flipud_out()
        rows, cols, image_str = im.as_rgba_str()

        image_array = np.fromstring(image_str, np.uint8)
        image_array.shape = rows, cols, 4

        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
                                has_alpha=True,
                                bits_per_sample=8,
                                width=cols,
                                height=rows)

        array = pixbuf_get_pixels_array(pixbuf)
        array[:, :, :] = image_array

        gc = self.new_gc()

        y = self.height - y - rows

        try:  # new in 2.2
            # can use None instead of gc.gdkGC, if don't need clipping
            self.gdkDrawable.draw_pixbuf(gc.gdkGC, pixbuf, 0, 0, int(x),
                                         int(y), cols, rows,
                                         gdk.RGB_DITHER_NONE, 0, 0)
        except AttributeError:
            # deprecated in 2.2
            pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0, int(x),
                                      int(y), cols, rows, gdk.RGB_DITHER_NONE,
                                      0, 0)

        # unflip
        im.flipud_out()

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

        if x < 0 or y < 0:  # window has shrunk and text is off the edge
            return

        if angle not in (0, 90):
            warnings.warn('backend_gdk: unable to draw text at angles ' +
                          'other than 0 or 90')
        elif ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        elif angle == 90:
            self._draw_rotated_text(gc, x, y, s, prop, angle)

        else:
            layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
            l, b, w, h = inkRect
            if (x + w > self.width or y + h > self.height):
                return

            self.gdkDrawable.draw_layout(gc.gdkGC, x, y - h - b, layout)

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

        if angle == 90:
            width, height = height, width
            x -= width
        y -= height

        imw = font_image.get_width()
        imh = font_image.get_height()
        N = imw * imh

        # a numpixels by num fonts array
        Xall = np.zeros((N, 1), np.uint8)

        image_str = font_image.as_str()
        Xall[:, 0] = np.fromstring(image_str, np.uint8)

        # get the max alpha at each pixel
        Xs = np.amax(Xall, axis=1)

        # convert it to it's proper shape
        Xs.shape = imh, imw

        pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
                                has_alpha=True,
                                bits_per_sample=8,
                                width=imw,
                                height=imh)

        array = pixbuf_get_pixels_array(pixbuf)

        rgb = gc.get_rgb()
        array[:, :, 0] = int(rgb[0] * 255)
        array[:, :, 1] = int(rgb[1] * 255)
        array[:, :, 2] = int(rgb[2] * 255)
        array[:, :, 3] = Xs

        try:  # new in 2.2
            # can use None instead of gc.gdkGC, if don't need clipping
            self.gdkDrawable.draw_pixbuf(gc.gdkGC, pixbuf, 0, 0, int(x),
                                         int(y), imw, imh, gdk.RGB_DITHER_NONE,
                                         0, 0)
        except AttributeError:
            # deprecated in 2.2
            pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0, int(x),
                                      int(y), imw, imh, gdk.RGB_DITHER_NONE, 0,
                                      0)

    def _draw_rotated_text(self, gc, x, y, s, prop, angle):
        """
        Draw the text rotated 90 degrees, other angles are not supported
        """
        # this function (and its called functions) is a bottleneck
        # Pango 1.6 supports rotated text, but pygtk 2.4.0 does not yet have
        # wrapper functions
        # GTK+ 2.6 pixbufs support rotation

        gdrawable = self.gdkDrawable
        ggc = gc.gdkGC

        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
        l, b, w, h = inkRect
        x = int(x - h)
        y = int(y - w)

        if (x < 0 or y < 0 or  # window has shrunk and text is off the edge
                x + w > self.width or y + h > self.height):
            return

        key = (x, y, s, angle, hash(prop))
        imageVert = self.rotated.get(key)
        if imageVert != None:
            gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
            return

        imageBack = gdrawable.get_image(x, y, w, h)
        imageVert = gdrawable.get_image(x, y, h, w)
        imageFlip = gtk.gdk.Image(type=gdk.IMAGE_FASTEST,
                                  visual=gdrawable.get_visual(),
                                  width=w,
                                  height=h)
        if imageFlip == None or imageBack == None or imageVert == None:
            warnings.warn("Could not renderer vertical text")
            return
        imageFlip.set_colormap(self._cmap)
        for i in range(w):
            for j in range(h):
                imageFlip.put_pixel(i, j, imageVert.get_pixel(j, w - i - 1))

        gdrawable.draw_image(ggc, imageFlip, 0, 0, x, y, w, h)
        gdrawable.draw_layout(ggc, x, y - b, layout)

        imageIn = gdrawable.get_image(x, y, w, h)
        for i in range(w):
            for j in range(h):
                imageVert.put_pixel(j, i, imageIn.get_pixel(w - i - 1, j))

        gdrawable.draw_image(ggc, imageBack, 0, 0, x, y, w, h)
        gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w)
        self.rotated[key] = imageVert

    def _get_pango_layout(self, s, prop):
        """
        Create a pango layout instance for Text 's' with properties 'prop'.
        Return - pango layout (from cache if already exists)

        Note that pango assumes a logical DPI of 96
        Ref: pango/fonts.c/pango_font_description_set_size() manual page
        """
        # problem? - cache gets bigger and bigger, is never cleared out
        # two (not one) layouts are created for every text item s (then they
        # are cached) - why?

        key = self.dpi, s, hash(prop)
        value = self.layoutd.get(key)
        if value != None:
            return value

        size = prop.get_size_in_points() * self.dpi / 96.0
        size = round(size)

        font_str = '%s, %s %i' % (
            prop.get_name(),
            prop.get_style(),
            size,
        )
        font = pango.FontDescription(font_str)

        # later - add fontweight to font_str
        font.set_weight(self.fontweights[prop.get_weight()])

        layout = self.gtkDA.create_pango_layout(s)
        layout.set_font_description(font)
        inkRect, logicalRect = layout.get_pixel_extents()

        self.layoutd[key] = layout, inkRect, logicalRect
        return layout, inkRect, logicalRect

    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:
            ox, oy, width, height, descent, font_image, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        layout, inkRect, logicalRect = self._get_pango_layout(s, prop)
        l, b, w, h = inkRect
        ll, lb, lw, lh = logicalRect

        return w, h + 1, h - lh

    def new_gc(self):
        return GraphicsContextGDK(renderer=self)

    def points_to_pixels(self, points):
        return points / 72.0 * self.dpi
예제 #34
0
class TextToPath(object):
    """
    A class that convert a given text to a path using ttf fonts.
    """

    FONT_SCALE = 50.
    DPI = 72

    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

    def _get_font(self, prop):
        """
        find a ttf font.
        """
        fname = font_manager.findfont(prop)
        font = FT2Font(str(fname))
        font.set_size(self.FONT_SCALE, self.DPI)

        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        ps_name = font.get_sfnt()[(1, 0, 0, 6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ccode))
        return char_id

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.quote('%s-%d' % (ps_name, ccode))
        return char_id

    def glyph_to_path(self, glyph, currx=0.):
        """
        convert the ft2font glyph to vertices and codes.
        """
        #Mostly copied from backend_svg.py.

        verts, codes = [], []
        for step in glyph.path:
            if step[0] == 0:  # MOVE_TO
                verts.append((step[1], step[2]))
                codes.append(Path.MOVETO)
            elif step[0] == 1:  # LINE_TO
                verts.append((step[1], step[2]))
                codes.append(Path.LINETO)
            elif step[0] == 2:  # CURVE3
                verts.extend([(step[1], step[2]), (step[3], step[4])])
                codes.extend([Path.CURVE3, Path.CURVE3])
            elif step[0] == 3:  # CURVE4
                verts.extend([(step[1], step[2]), (step[3], step[4]),
                              (step[5], step[6])])
                codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
            elif step[0] == 4:  # ENDPOLY
                verts.append((
                    0,
                    0,
                ))
                codes.append(Path.CLOSEPOLY)

        verts = [(x + currx, y) for (x, y) in verts]
        return verts, codes

    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        convert text *s* to path (a tuple of vertices and codes for matplotlib.math.Path).

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. Effective only if usetex == False.


        """
        if usetex == False:
            if ismath == False:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(
                    font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
                    prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if verts1:
                verts1 = np.array(verts1) * scale + [xposition, yposition]
            verts.extend(verts1)
            codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self,
                             font,
                             s,
                             glyph_map=None,
                             return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using the
        provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        cmap = font.get_charmap()
        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:

            ccode = ord(c)
            gind = cmap.get(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = (glyph.linearHoriAdvance / 65536.0)

            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                glyph_map_new[char_id] = self.glyph_to_path(glyph)

            currx += (kern / 64.0)

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return zip(glyph_ids, xpositions, ypositions,
                   sizes), glyph_map_new, rects

    def get_glyphs_mathtext(self,
                            prop,
                            s,
                            glyph_map=None,
                            return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes by parsing it with mathtext.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        currx, curry = 0, 0
        for font, fontsize, s, ox, oy in glyphs:

            ccode = ord(s)
            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = self.glyph_to_path(glyph)

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))

        return zip(glyph_ids, xpositions, ypositions,
                   sizes), glyph_map, myrects

    def get_texmanager(self):
        """
        return the :class:`matplotlib.texmanager.TexManager` instance
        """
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self,
                       prop,
                       s,
                       glyph_map=None,
                       return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using matplotlib's usetex mode.
        """

        # codes are modstly borrowed from pdf backend.

        texmanager = self.get_texmanager()

        if self.tex_font_map is None:
            self.tex_font_map = dviread.PsfontsMap(
                dviread.find_tex_file('pdftex.map'))

        fontsize = prop.get_size_in_points()
        if hasattr(texmanager, "get_dvi"):  #
            dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
            dvi = dviread.DviFromFileLike(dvifilelike, self.DPI)
        else:
            dvifile = texmanager.make_dvi(s, self.FONT_SCALE)
            dvi = dviread.Dvi(dvifile, self.DPI)
        page = next(iter(dvi))
        dvi.close()

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        #oldfont, seq = None, []
        for x1, y1, dvifont, glyph, width in page.text:
            font_and_encoding = self._ps_fontd.get(dvifont.texname)

            if font_and_encoding is None:
                font_bunch = self.tex_font_map[dvifont.texname]
                font = FT2Font(str(font_bunch.filename))
                try:
                    font.select_charmap(1094992451)  # select ADOBE_CUSTOM
                except ValueError:
                    font.set_charmap(0)
                if font_bunch.encoding:
                    enc = dviread.Encoding(font_bunch.encoding)
                else:
                    enc = None
                self._ps_fontd[dvifont.texname] = font, enc

            else:
                font, enc = font_and_encoding

            ft2font_flag = LOAD_TARGET_LIGHT

            char_id = self._get_char_id_ps(font, glyph)

            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)

                glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = self.glyph_to_path(glyph0)

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))


        return zip(glyph_ids, xpositions, ypositions, sizes), \
               glyph_map_new, myrects
예제 #35
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
예제 #36
0
class RendererCairo(RendererBase):
    fontweights = {
        100: cairo.FONT_WEIGHT_NORMAL,
        200: cairo.FONT_WEIGHT_NORMAL,
        300: cairo.FONT_WEIGHT_NORMAL,
        400: cairo.FONT_WEIGHT_NORMAL,
        500: cairo.FONT_WEIGHT_NORMAL,
        600: cairo.FONT_WEIGHT_BOLD,
        700: cairo.FONT_WEIGHT_BOLD,
        800: cairo.FONT_WEIGHT_BOLD,
        900: cairo.FONT_WEIGHT_BOLD,
        'ultralight': cairo.FONT_WEIGHT_NORMAL,
        'light': cairo.FONT_WEIGHT_NORMAL,
        'normal': cairo.FONT_WEIGHT_NORMAL,
        'medium': cairo.FONT_WEIGHT_NORMAL,
        'semibold': cairo.FONT_WEIGHT_BOLD,
        'bold': cairo.FONT_WEIGHT_BOLD,
        'heavy': cairo.FONT_WEIGHT_BOLD,
        'ultrabold': cairo.FONT_WEIGHT_BOLD,
        'black': cairo.FONT_WEIGHT_BOLD,
    }
    fontangles = {
        'italic': cairo.FONT_SLANT_ITALIC,
        'normal': cairo.FONT_SLANT_NORMAL,
        'oblique': cairo.FONT_SLANT_OBLIQUE,
    }

    def __init__(self, dpi):
        """
        """
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
            cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        self.mathtext_parser = MathTextParser('Cairo')

        RendererBase.__init__(self)

    def set_ctx_from_surface(self, surface):
        self.gc.ctx = cairo.Context(surface)

    def set_width_height(self, width, height):
        self.width = width
        self.height = height
        self.matrix_flipy = cairo.Matrix(yy=-1, y0=self.height)
        # use matrix_flipy for ALL rendering?
        # - problem with text? - will need to switch matrix_flipy off, or do a
        # font transform?

    def _fill_and_stroke(self, ctx, fill_c, alpha):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2],
                                    alpha * fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    def convert_path(ctx, path, transform):
        for points, code in path.iter_segments(transform):
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1], points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)

    def draw_path(self, gc, path, transform, rgbFace=None):
        if len(path.vertices) > 18980:
            raise ValueError(
                "The Cairo backend can not draw paths longer than 18980 points."
            )

        ctx = gc.ctx

        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)

        ctx.new_path()
        self.convert_path(ctx, path, transform)

        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha())

    def draw_image(self, gc, x, y, im):
        # bbox - not currently used
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        im.flipud_out()

        rows, cols, buf = im.color_conv(BYTE_FORMAT)
        surface = cairo.ImageSurface.create_for_data(buf, cairo.FORMAT_ARGB32,
                                                     cols, rows, cols * 4)
        ctx = gc.ctx
        y = self.height - y - rows
        ctx.set_source_surface(surface, x, y)
        ctx.paint()

        im.flipud_out()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

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

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to(x, y)
            ctx.select_font_face(prop.get_name(),
                                 self.fontangles[prop.get_style()],
                                 self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate(-angle * np.pi / 180)
            ctx.set_font_size(size)
            if sys.version_info[0] < 3:
                ctx.show_text(s.encode("utf-8"))
            else:
                ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate(-angle * np.pi / 180)

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.save()
            ctx.select_font_face(fontProp.name,
                                 self.fontangles[fontProp.style],
                                 self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            if sys.version_info[0] < 3:
                ctx.show_text(s.encode("utf-8"))
            else:
                ctx.show_text(s)
            ctx.restore()

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle(ox, oy, w, h)
            ctx.set_source_rgb(0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()

    def flipy(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return True
        #return False # tried - all draw objects ok except text (and images?)
        # which comes out mirrored!

    def get_canvas_width_height(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
                s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face(prop.get_name(),
                             self.fontangles[prop.get_style()],
                             self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small

        size = prop.get_size_in_points() * self.dpi / 72.0

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size(size)

        y_bearing, w, h = ctx.text_extents(s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing

    def new_gc(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.gc.ctx.save()
        self.gc._alpha = 1.0
        self.gc._forced_alpha = False  # if True, _alpha overrides A from RGBA
        return self.gc

    def points_to_pixels(self, points):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return points / 72.0 * self.dpi
예제 #37
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_hatch_color()
        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)
예제 #38
0
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[:3])
        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[:3])
        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, gc.get_clip_rectangle(),
                      *gc.get_clip_path())
        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
예제 #39
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        """
        """
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.dpi = dpi
        self.text_ctx = cairo.Context (
           cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1))
        self.mathtext_parser = MathTextParser('Cairo')

    def set_ctx_from_surface (self, surface):
       self.ctx = cairo.Context (surface)
       self.ctx.save() # restore, save  - when call new_gc()


    def set_width_height(self, width, height):
        self.width  = width
        self.height = height
        self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height)
        # use matrix_flipy for ALL rendering?
        # - problem with text? - will need to switch matrix_flipy off, or do a
        # font transform?


    def _fill_and_stroke (self, ctx, fill_c):
        #assert fill_c or stroke_c

        #_.ctx.save()

        if fill_c:
            ctx.save()
            ctx.set_source_rgb (*fill_c)
            #if stroke_c:   # always (implicitly) set at the moment
            ctx.fill_preserve()
            #else:
            #    ctx.fill()
            ctx.restore()

        #if stroke_c:                      # always stroke
            #ctx.set_source_rgb (stroke_c) # is already set
        ctx.stroke()

        #_.ctx.restore() # revert to the default attributes


    def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2,
                 rotation):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        ctx = gc.ctx
        ctx.save()
        ctx.translate(x, self.height - y)
        ctx.rotate(rotation)
        ctx.scale(width / 2.0, height / 2.0)
        ctx.new_sub_path()
        ctx.arc(0.0, 0.0, 1.0, npy.pi * angle1 / 180.,
                npy.pi * angle2 / 180.)
        ctx.restore()

        self._fill_and_stroke (ctx, rgbFace)


    def draw_image(self, x, y, im, bbox):
        # bbox - not currently used
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        im.flipud_out()

        rows, cols, buf = im.color_conv (BYTE_FORMAT)
        surface = cairo.ImageSurface.create_for_data (
                      buf, cairo.FORMAT_ARGB32, cols, rows, cols*4)
        # function does not pass a 'gc' so use renderer.ctx
        ctx = self.ctx
        y = self.height - y - rows
        ctx.set_source_surface (surface, x, y)
        ctx.paint()

        im.flipud_out()


    def draw_line(self, gc, x1, y1, x2, y2):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        ctx = gc.ctx
        ctx.new_path()
        ctx.move_to (x1, self.height - y1)
        ctx.line_to (x2, self.height - y2)
        self._fill_and_stroke (ctx, None)


    def draw_lines(self, gc, x, y, transform=None):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        if transform:
            if transform.need_nonlinear():
                x, y = transform.nonlinear_only_numerix(x, y)
            x, y = transform.numerix_x_y(x, y)

        ctx = gc.ctx
        matrix_old = ctx.get_matrix()
        ctx.set_matrix (self.matrix_flipy)

        points = izip(x,y)
        x, y = points.next()
        ctx.new_path()
        ctx.move_to (x, y)

        for x,y in points:
            ctx.line_to (x, y)
        self._fill_and_stroke (ctx, None)

        ctx.set_matrix (matrix_old)


    def draw_markers_OLD(self, gc, path, rgbFace, x, y, transform):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        ctx = gc.ctx

        if transform.need_nonlinear():
            x,y = transform.nonlinear_only_numerix(x, y)

        x, y = transform.numerix_x_y(x, y) # do nonlinear and affine transform

        # TODO - use cairo transform
        # matrix worked for dotted lines, but not markers in line_styles.py
        # it upsets/transforms generate_path() ?
        # need to flip y too, and update generate_path() ?
        # the a,b,c,d,tx,ty affine which transforms x and y
        #vec6 = transform.as_vec6_val() # not used (yet)
        #matrix_old = ctx.get_matrix()
        #ctx.set_matrix (cairo.Matrix (*vec6))

        path_list = [path.vertex() for i in range(path.total_vertices())]

        def generate_path (path_list):
           for code, xp, yp in path_list:
               if code == agg.path_cmd_move_to:
                  ctx.move_to (xp, -yp)
               elif code == agg.path_cmd_line_to:
                  ctx.line_to (xp, -yp)
               elif code == agg.path_cmd_end_poly:
                  ctx.close_path()

        for x,y in izip(x,y):
            ctx.save()
            ctx.new_path()
            ctx.translate(x, self.height - y)
            generate_path (path_list)

            self._fill_and_stroke (ctx, rgbFace)

            ctx.restore() # undo translate()

        #ctx.set_matrix(matrix_old)


    def draw_point(self, gc, x, y):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        # render by drawing a 0.5 radius circle
        ctx = gc.ctx
        ctx.new_path()
        ctx.arc (x, self.height - y, 0.5, 0, 2*npy.pi)
        self._fill_and_stroke (ctx, gc.get_rgb())


    def draw_polygon(self, gc, rgbFace, points):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        ctx = gc.ctx
        matrix_old = ctx.get_matrix()
        ctx.set_matrix (self.matrix_flipy)

        ctx.new_path()
        x, y = points[0]
        ctx.move_to (x, y)
        for x,y in points[1:]:
            ctx.line_to (x, y)
        ctx.close_path()

        self._fill_and_stroke (ctx, rgbFace)

        ctx.set_matrix (matrix_old)

    def draw_rectangle(self, gc, rgbFace, x, y, width, height):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        ctx = gc.ctx
        ctx.new_path()
        ctx.rectangle (x, self.height - y - height, width, height)
        self._fill_and_stroke (ctx, rgbFace)


    def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

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

        else:
           ctx = gc.ctx
           ctx.new_path()
           ctx.move_to (x, y)
           ctx.select_font_face (prop.get_name(),
                                 self.fontangles [prop.get_style()],
                                 self.fontweights[prop.get_weight()])

           # size = prop.get_size_in_points() * self.dpi.get() / 96.0
           size = prop.get_size_in_points() * self.dpi.get() / 72.0

           ctx.save()
           if angle:
              ctx.rotate (-angle * npy.pi / 180)
           ctx.set_font_size (size)
           ctx.show_text (s.encode("utf-8"))
           ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())

        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi.get(), prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
           ctx.rotate (-angle * npy.pi / 180)
           
        for font, fontsize, s, ox, oy in glyphs:
           ctx.new_path()
           ctx.move_to(ox, oy)
           
           fontProp = ttfFontProperty(font)
           ctx.save()
           ctx.select_font_face (fontProp.name,
                                 self.fontangles [fontProp.style],
                                 self.fontweights[fontProp.weight])

           # size = prop.get_size_in_points() * self.dpi.get() / 96.0
           size = fontsize * self.dpi.get() / 72.0
           ctx.set_font_size(size)
           ctx.show_text(s.encode("utf-8"))
           ctx.restore()

        for ox, oy, w, h in rects:
           ctx.new_path()
           ctx.rectangle (ox, oy, w, h)
           ctx.set_source_rgb (0, 0, 0)
           ctx.fill_preserve()

        ctx.restore()

        
    def flipy(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return True
        #return False # tried - all draw objects ok except text (and images?)
        # which comes out mirrored!


    def get_canvas_width_height(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return self.width, self.height


    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
               s, self.dpi.get(), prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],
                              self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small

        #size = prop.get_size_in_points() * self.dpi.get() / 96.0
        size = prop.get_size_in_points() * self.dpi.get() / 72.0

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size (size)

        y_bearing, w, h = ctx.text_extents (s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing


    def new_gc(self):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        self.ctx.restore()  # matches save() in set_ctx_from_surface()
        self.ctx.save()
        return GraphicsContextCairo (renderer=self)


    def points_to_pixels(self, points):
        if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name())
        return points/72.0 * self.dpi.get()
예제 #40
0
class TextToPath:
    """A class that converts strings to paths."""

    FONT_SCALE = 100.
    DPI = 72

    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self._texmanager = None

    def _get_font(self, prop):
        """
        Find the `FT2Font` matching font properties *prop*, with its size set.
        """
        fname = font_manager.findfont(prop)
        font = get_font(fname)
        font.set_size(self.FONT_SCALE, self.DPI)
        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode))

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode))
        return char_id

    @cbook.deprecated(
        "3.1",
        alternative="font.get_path() and manual translation of the vertices")
    def glyph_to_path(self, font, currx=0.):
        """Convert the *font*'s current glyph to a (vertices, codes) pair."""
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = fontsize / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    @cbook._delete_parameter("3.1", "usetex")
    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        Convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        Parameters
        ----------
        prop : `matplotlib.font_manager.FontProperties` instance
            The font properties for the text.

        s : str
            The text to be converted.

        ismath : {False, True, "TeX"}
            If True, use mathtext parser.  If "TeX", use tex for renderering.

        usetex : bool, optional
            If set, forces *ismath* to True.  This parameter is deprecated.

        Returns
        -------
        verts, codes : tuple of lists
            *verts*  is a list of numpy arrays containing the x and y
            coordinates of the vertices. *codes* is a list of path codes.

        Examples
        --------
        Create a list of vertices and codes from a text, and create a `Path`
        from those::

            from matplotlib.path import Path
            from matplotlib.textpath import TextToPath
            from matplotlib.font_manager import FontProperties

            fp = FontProperties(family="Humor Sans", style="italic")
            verts, codes = TextToPath().get_text_path(fp, "ABC")
            path = Path(verts, codes, closed=False)

        Also see `TextPath` for a more direct way to create a path from a text.
        """
        if usetex:
            ismath = "TeX"
        if ismath == "TeX":
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
        elif not ismath:
            font = self._get_font(prop)
            glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self, font, s, glyph_map=None,
                             return_new_glyphs_only=False):
        """
        Convert string *s* to vertices and codes using the provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:
            ccode = ord(c)
            gind = font.get_char_index(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = glyph.linearHoriAdvance / 65536

            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                glyph_map_new[char_id] = font.get_path()

            currx += kern / 64

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, rects)

    def get_glyphs_mathtext(self, prop, s, glyph_map=None,
                            return_new_glyphs_only=False):
        """
        Parse mathtext string *s* and convert it to a (vertices, codes) pair.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = font.get_path()

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
                     (ox + w, oy), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)

    def get_texmanager(self):
        """Return the cached `~.texmanager.TexManager` instance."""
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self, prop, s, glyph_map=None,
                       return_new_glyphs_only=False):
        """Convert the string *s* to vertices and codes using usetex mode."""
        # Mostly borrowed from pdf backend.

        dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
        with dviread.Dvi(dvifile, self.DPI) as dvi:
            page, = dvi

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        for x1, y1, dvifont, glyph, width in page.text:
            font, enc = self._get_ps_font_and_encoding(dvifont.texname)
            char_id = self._get_char_id_ps(font, glyph)

            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                # See comments in _get_ps_font_and_encoding.
                if enc is not None:
                    index = font.get_name_index(enc[glyph])
                    font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
                else:
                    font.load_char(glyph, flags=LOAD_TARGET_LIGHT)
                glyph_map_new[char_id] = font.get_path()

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
                     (ox, oy + h), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)

    @staticmethod
    @functools.lru_cache(50)
    def _get_ps_font_and_encoding(texname):
        tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
        font_bunch = tex_font_map[texname]
        if font_bunch.filename is None:
            raise ValueError(
                f"No usable font file found for {font_bunch.psname} "
                f"({texname}). The font may lack a Type-1 version.")

        font = get_font(font_bunch.filename)

        if font_bunch.encoding:
            # If psfonts.map specifies an encoding, use it: it gives us a
            # mapping of glyph indices to Adobe glyph names; use it to convert
            # dvi indices to glyph names and use the FreeType-synthesized
            # unicode charmap to convert glyph names to glyph indices (with
            # FT_Get_Name_Index/get_name_index), and load the glyph using
            # FT_Load_Glyph/load_glyph.  (That charmap has a coverage at least
            # as good as, and possibly better than, the native charmaps.)
            enc = dviread._parse_enc(font_bunch.encoding)
        else:
            # If psfonts.map specifies no encoding, the indices directly
            # map to the font's "native" charmap; so don't use the
            # FreeType-synthesized charmap but the native ones (we can't
            # directly identify it but it's typically an Adobe charmap), and
            # directly load the dvi glyph indices using FT_Load_Char/load_char.
            for charmap_code in [
                    1094992451,  # ADOBE_CUSTOM.
                    1094995778,  # ADOBE_STANDARD.
            ]:
                try:
                    font.select_charmap(charmap_code)
                except (ValueError, RuntimeError):
                    pass
                else:
                    break
            else:
                _log.warning("No supported encoding in font (%s).",
                             font_bunch.filename)
            enc = None

        return font, enc
예제 #41
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):
        path = transform.transform_path(path)
        for points, code in path.iter_segments():
            if code == Path.MOVETO:
                gc.moveto(points)
            elif code == Path.LINETO:
                gc.lineto(points)
            elif code == Path.CURVE3:
                gc.curve3(points)
            elif code == Path.CURVE4:
                gc.curve4(points)
            elif code == Path.CLOSEPOLY:
                gc.closepoly()
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        gc.stroke(rgbFace)

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

    def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None):
        self.gc.set_clip_rectangle(bbox)
        im.flipud_out()
        nrows, ncols, data = im.as_rgba_str()
        self.gc.draw_image(x, y, nrows, ncols, data)
        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):
        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 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
예제 #42
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)
        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, *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.save()
        self.gc.set_hatch(None)
        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):
        # 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):
        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
예제 #43
0
파일: backend_gr.py 프로젝트: Juanlu001/gr
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
        self.width = float(width) * dpi / 80
        self.height = float(height) * dpi / 80
        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:
            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):
        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)

    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 * 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
예제 #44
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'regular'    : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
           cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        self.mathtext_parser = MathTextParser('Cairo')
        RendererBase.__init__(self)

    def set_ctx_from_surface(self, surface):
        self.gc.ctx = cairo.Context(surface)
        # Although it may appear natural to automatically call
        # `self.set_width_height(surface.get_width(), surface.get_height())`
        # here (instead of having the caller do so separately), this would fail
        # for PDF/PS/SVG surfaces, which have no way to report their extents.

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

    def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    def convert_path(ctx, path, transform, clip=None):
        for points, code in path.iter_segments(transform, clip=clip):
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1],
                             points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)

    def draw_path(self, gc, path, transform, rgbFace=None):
        ctx = gc.ctx

        # We'll clip the path to the actual rendering extents
        # if the path isn't filled.
        if rgbFace is None and gc.get_hatch() is None:
            clip = ctx.clip_extents()
        else:
            clip = None

        transform = (transform
                     + Affine2D().scale(1.0, -1.0).translate(0, self.height))

        ctx.new_path()
        self.convert_path(ctx, path, transform, clip)

        self._fill_and_stroke(
            ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_markers(self, gc, marker_path, marker_trans, path, transform,
                     rgbFace=None):
        ctx = gc.ctx

        ctx.new_path()
        # Create the path for the marker; it needs to be flipped here already!
        self.convert_path(
            ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
        marker_path = ctx.copy_path_flat()

        # Figure out whether the path has a fill
        x1, y1, x2, y2 = ctx.fill_extents()
        if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
            filled = False
            # No fill, just unset this (so we don't try to fill it later on)
            rgbFace = None
        else:
            filled = True

        transform = (transform
                     + Affine2D().scale(1.0, -1.0).translate(0, self.height))

        ctx.new_path()
        for i, (vertices, codes) in enumerate(
                path.iter_segments(transform, simplify=False)):
            if len(vertices):
                x, y = vertices[-2:]
                ctx.save()

                # Translate and apply path
                ctx.translate(x, y)
                ctx.append_path(marker_path)

                ctx.restore()

                # Slower code path if there is a fill; we need to draw
                # the fill and stroke for each marker at the same time.
                # Also flush out the drawing every once in a while to
                # prevent the paths from getting way too long.
                if filled or i % 1000 == 0:
                    self._fill_and_stroke(
                        ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

        # Fast path, if there is no fill, draw everything in one step
        if not filled:
            self._fill_and_stroke(
                ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_image(self, gc, x, y, im):
        # bbox - not currently used
        if sys.byteorder == 'little':
            im = im[:, :, (2, 1, 0, 3)]
        else:
            im = im[:, :, (3, 0, 1, 2)]
        if HAS_CAIRO_CFFI:
            # cairocffi tries to use the buffer_info from array.array
            # that we replicate in ArrayWrapper and alternatively falls back
            # on ctypes to get a pointer to the numpy array. This works
            # correctly on a numpy array in python3 but not 2.7. We replicate
            # the array.array functionality here to get cross version support.
            imbuffer = ArrayWrapper(im.flatten())
        else:
            # pycairo uses PyObject_AsWriteBuffer to get a pointer to the
            # numpy array; this works correctly on a regular numpy array but
            # not on a py2 memoryview.
            imbuffer = im.flatten()
        surface = cairo.ImageSurface.create_for_data(
            imbuffer, cairo.FORMAT_ARGB32,
            im.shape[1], im.shape[0], im.shape[1]*4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        if gc.get_alpha() != 1.0:
            ctx.paint_with_alpha(gc.get_alpha())
        else:
            ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if ismath:
            self._draw_mathtext(gc, x, y, s, prop, angle)

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to(x, y)
            ctx.select_font_face(prop.get_name(),
                                 self.fontangles[prop.get_style()],
                                 self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate(np.deg2rad(-angle))
            ctx.set_font_size(size)

            if HAS_CAIRO_CFFI:
                if not isinstance(s, six.text_type):
                    s = six.text_type(s)
            else:
                if six.PY2 and isinstance(s, six.text_type):
                    s = s.encode("utf-8")

            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate(np.deg2rad(-angle))

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.save()
            ctx.select_font_face(fontProp.name,
                                 self.fontangles[fontProp.style],
                                 self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            if not six.PY3 and isinstance(s, six.text_type):
                s = s.encode("utf-8")
            ctx.show_text(s)
            ctx.restore()

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle(ox, oy, w, h)
            ctx.set_source_rgb(0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()

    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, fonts, used_characters = \
                self.mathtext_parser.parse(s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face(prop.get_name(),
                             self.fontangles[prop.get_style()],
                             self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small
        size = prop.get_size_in_points() * self.dpi / 72

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size(size)

        y_bearing, w, h = ctx.text_extents(s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing

    def new_gc(self):
        self.gc.ctx.save()
        self.gc._alpha = 1
        self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
        return self.gc

    def points_to_pixels(self, points):
        return points / 72 * self.dpi
예제 #45
0
class RendererAgg(RendererBase):
    """
    The renderer handles all the drawing primitives using a graphics
    context instance that controls the colors/styles
    """

    # 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 time and so the font cache is used by only one
    # renderer at a time.

    lock = threading.RLock()

    def __init__(self, width, height, dpi):
        RendererBase.__init__(self)

        self.dpi = dpi
        self.width = width
        self.height = height
        self._renderer = _RendererAgg(int(width), int(height), dpi)
        self._filter_renderers = []

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

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)

    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 _update_methods(self):
        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.draw_markers = self._renderer.draw_markers
        # This is its own method for the duration of the deprecation of
        # offset_position = "data".
        # self.draw_path_collection = self._renderer.draw_path_collection
        self.draw_quad_mesh = self._renderer.draw_quad_mesh
        self.copy_from_bbox = self._renderer.copy_from_bbox

    @cbook.deprecated("3.4")
    def get_content_extents(self):
        orig_img = np.asarray(self.buffer_rgba())
        slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3])
        return (slice_x.start, slice_y.start, slice_x.stop - slice_x.start,
                slice_y.stop - slice_y.start)

    @cbook.deprecated("3.4")
    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):
        # docstring inherited
        nmax = mpl.rcParams['agg.path.chunksize']  # here at least for testing
        npts = path.vertices.shape[0]

        if (npts > nmax > 100 and path.should_simplify and rgbFace is None
                and gc.get_hatch() is None):
            nch = np.ceil(npts / 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)
                try:
                    self._renderer.draw_path(gc, p, transform, rgbFace)
                except OverflowError as err:
                    raise OverflowError(
                        "Exceeded cell block limit (set 'agg.path.chunksize' "
                        "rcparam)") from err
        else:
            try:
                self._renderer.draw_path(gc, path, transform, rgbFace)
            except OverflowError as err:
                raise OverflowError("Exceeded cell block limit (set "
                                    "'agg.path.chunksize' rcparam)") from err

    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":
            cbook.warn_deprecated(
                "3.3",
                message="Support for offset_position='data' is "
                "deprecated since %(since)s and will be removed %(removal)s.")
        return self._renderer.draw_path_collection(gc, master_transform, paths,
                                                   all_transforms, offsets,
                                                   offsetTrans, facecolors,
                                                   edgecolors, linewidths,
                                                   linestyles, antialiaseds,
                                                   urls, offset_position)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """Draw mathtext using :mod:`matplotlib.mathtext`."""
        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):
        # docstring inherited

        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
        # 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=mpl.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))
        x = round(x + xo + xd)
        y = round(y + yo + yd)
        self._renderer.draw_text_image(font, x, y + 1, angle, gc)

    def get_text_width_height_descent(self, s, prop, ismath):
        # docstring inherited

        if ismath in ["TeX", "TeX!"]:
            if ismath == "TeX!":
                cbook._warn_deprecated(
                    "3.3",
                    message="Support for ismath='TeX!' is deprecated "
                    "since %(since)s and will be removed %(removal)s; use "
                    "ismath='TeX' instead.")
            # 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

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

    @cbook._delete_parameter("3.2", "ismath")
    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        # docstring inherited
        # 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="TeX")
        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):
        # docstring inherited
        return self.width, self.height

    def _get_agg_font(self, prop):
        """
        Get the font for text instance t, caching for efficiency
        """
        fname = findfont(prop)
        font = get_font(fname)

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

        return font

    def points_to_pixels(self, points):
        # docstring inherited
        return points * self.dpi / 72

    def buffer_rgba(self):
        return memoryview(self._renderer)

    def tostring_argb(self):
        return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()

    def tostring_rgb(self):
        return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()

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

    def option_image_nocomposite(self):
        # docstring inherited

        # 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):
        # docstring inherited
        return False

    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 pair of floats) 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.
        """
        orig_img = np.asarray(self.buffer_rgba())
        slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3])
        cropped_img = orig_img[slice_y, slice_x]

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

        if cropped_img.size:
            img, ox, oy = post_processing(cropped_img / 255, self.dpi)
            gc = self.new_gc()
            if img.dtype.kind == 'f':
                img = np.asarray(img * 255., np.uint8)
            self._renderer.draw_image(gc, slice_x.start + ox,
                                      int(self.height) - slice_y.stop + oy,
                                      img[::-1])
예제 #46
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0

    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')
        self.fontd = {}
        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 _get_font(self, prop):
        key = hash(prop)
        font = self.fontd.get(key)
        if font is None:
            fname = findfont(prop)
            font = FT2Font(str(fname))
            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: %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()),
                         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()
        if cliprect is None:
            return '', None
        else:
            # 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)s" y="%(y)s" width="%(w)s" height="%(h)s"
    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_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation):
        """
        Ignores angles for now
        """
        details = 'cx="%s" cy="%s" rx="%s" ry="%s" transform="rotate(%1.1f %s %s)"' % \
            (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(%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()

        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="%s" y="%s" width="%s" height="%s" '
            '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%s,%sL%s,%s"' % (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%s,%s' % (x[0], y[0])]
        xys = zip(x[1:], y[1:])
        details.extend(['L%s,%s' % 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(['%s,%s'%(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="%s" height="%s" x="%s" y="%s"' % (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())

        if rcParams['svg.embed_char_paths']:
            svg = ['<g style="fill: %s" transform="' % color]
            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:
                charid = self._add_char_def(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

                svg.append('<use xlink:href="#%s"' % charid)
                if currx != 0:
                    svg.append(' transform="translate(%s)"' %
                               (currx * (self.FONT_SCALE / fontsize)))
                svg.append('/>\n')
                currx += (glyph.linearHoriAdvance / 65536.0)
            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;'%(fontsize, fontfamily,fontstyle, color)
            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()
        self._svgwriter.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, path = self._char_defs.get(char_id, (None, None))
        if char_num is not None:
            return char_num

        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]
        char_num = 'c_%x' % len(self._char_defs)
        path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data))
        self._char_defs[char_id] = (char_num, path_element)
        return char_num

    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())

        self.open_group("mathtext")

        style = "fill: %s" % color

        if rcParams['svg.embed_char_paths']:
            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._add_char_def(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._svgwriter.write (''.join(svg))
        self.close_group("mathtext")

    def finish(self):
        write = self._svgwriter.write
        if len(self._char_defs):
            write('<defs id="fontpaths">\n')
            for char_num, path in self._char_defs.values():
                write(path)
            write('</defs>\n')
        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
예제 #47
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.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

        if gc.get_url() is not None:
            self._svgwriter.write('<a xlink:href="%s">' % gc.get_url())
        style = self._get_style(gc, rgbFace)
        self._svgwriter.write ('<%s style="%s" %s %s/>\n' % (
                element, style, clippath, details))
        if gc.get_url() is not None:
            self._svgwriter.write('</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(str(dictkey)).hexdigest()
            self._svgwriter.write('<defs>\n  <pattern id="%s" ' % id)
            self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ')
            self._svgwriter.write(' 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))
            if rgbFace is None:
                fill = 'none'
            else:
                fill = rgb2hex(rgbFace)
            self._svgwriter.write(
                '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' %
                (HATCH_SIZE+1, HATCH_SIZE+1, fill))
            path = '<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % (
                path_data, rgb2hex(gc.get_rgb()[:3]), rgb2hex(gc.get_rgb()[:3]))
            self._svgwriter.write(path)
            self._svgwriter.write('\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 = "url(#%s)" % self._get_hatch(gc, rgbFace)
        else:
            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: %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()[:3]),
                         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, 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)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % 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, 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('<g id="%s">\n' % (gid))
        else:
            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%f %f',
        Path.LINETO: 'L%f %f',
        Path.CURVE3: 'Q%f %f %f %f',
        Path.CURVE4: '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):
        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):
            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, clip=(rgbFace is None))
        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)
        for vertices, code in path.iter_segments(trans_and_flip, simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                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, urls):
        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, urls):
            clipid = self._get_gc_clip_svg(gc)
            url = gc.get_url()
            if url is not None:
                self._svgwriter.write('<a xlink:href="%s">' % url)
            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>')
            if url is not None:
                self._svgwriter.write('</a>')

        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())
            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()

        url = getattr(im, '_url', None)
        if url is not None:
            self._svgwriter.write('<a xlink:href="%s">' % url)
        self._svgwriter.write (
            '<image x="%f" y="%f" width="%f" height="%f" '
            '%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')
        if url is not None:
            self._svgwriter.write('</a>')

    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: %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
                currx += (kern / 64.0) / (self.FONT_SCALE / fontsize)

                svg.append('<use xlink:href="#%s"' % charnum)
                if currx != 0:
                    svg.append(' x="%f"' %
                               (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: %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(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(%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 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
예제 #48
0
class TextToPath:
    """A class that converts strings to paths."""

    FONT_SCALE = 100.
    DPI = 72

    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self._texmanager = None

    def _get_font(self, prop):
        """
        Find the `FT2Font` matching font properties *prop*, with its size set.
        """
        fname = font_manager.findfont(prop)
        font = get_font(fname)
        font.set_size(self.FONT_SCALE, self.DPI)
        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode))

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode))
        return char_id

    @cbook.deprecated(
        "3.1",
        alternative="font.get_path() and manual translation of the vertices")
    def glyph_to_path(self, font, currx=0.):
        """Convert the *font*'s current glyph to a (vertices, codes) pair."""
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = fontsize / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    def get_text_path(self, prop, s, ismath=False):
        """
        Convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        Parameters
        ----------
        prop : `~matplotlib.font_manager.FontProperties`
            The font properties for the text.

        s : str
            The text to be converted.

        ismath : {False, True, "TeX"}
            If True, use mathtext parser.  If "TeX", use tex for renderering.

        Returns
        -------
        verts, codes : tuple of lists
            *verts*  is a list of numpy arrays containing the x and y
            coordinates of the vertices. *codes* is a list of path codes.

        Examples
        --------
        Create a list of vertices and codes from a text, and create a `.Path`
        from those::

            from matplotlib.path import Path
            from matplotlib.textpath import TextToPath
            from matplotlib.font_manager import FontProperties

            fp = FontProperties(family="Humor Sans", style="italic")
            verts, codes = TextToPath().get_text_path(fp, "ABC")
            path = Path(verts, codes, closed=False)

        Also see `TextPath` for a more direct way to create a path from a text.
        """
        if ismath == "TeX":
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
        elif not ismath:
            font = self._get_font(prop)
            glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self,
                             font,
                             s,
                             glyph_map=None,
                             return_new_glyphs_only=False):
        """
        Convert string *s* to vertices and codes using the provided ttf font.
        """

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        glyph_ids = []
        for char, (_, x) in zip(s, _text_layout.layout(s, font)):
            char_id = self._get_char_id(font, ord(char))
            glyph_ids.append(char_id)
            xpositions.append(x)
            if char_id not in glyph_map:
                glyph_map_new[char_id] = font.get_path()

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, rects)

    def get_glyphs_mathtext(self,
                            prop,
                            s,
                            glyph_map=None,
                            return_new_glyphs_only=False):
        """
        Parse mathtext string *s* and convert it to a (vertices, codes) pair.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = font.get_path()

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, myrects)

    def get_texmanager(self):
        """Return the cached `~.texmanager.TexManager` instance."""
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self,
                       prop,
                       s,
                       glyph_map=None,
                       return_new_glyphs_only=False):
        """Convert the string *s* to vertices and codes using usetex mode."""
        # Mostly borrowed from pdf backend.

        dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
        with dviread.Dvi(dvifile, self.DPI) as dvi:
            page, = dvi

        if glyph_map is None:
            glyph_map = OrderedDict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        for x1, y1, dvifont, glyph, width in page.text:
            font, enc = self._get_ps_font_and_encoding(dvifont.texname)
            char_id = self._get_char_id_ps(font, glyph)

            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                # See comments in _get_ps_font_and_encoding.
                if enc is not None:
                    index = font.get_name_index(enc[glyph])
                    font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
                else:
                    font.load_char(glyph, flags=LOAD_TARGET_LIGHT)
                glyph_map_new[char_id] = font.get_path()

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h),
                     (ox, oy), (0, 0)]
            code1 = [
                Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
                Path.LINETO, Path.CLOSEPOLY
            ]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions,
                         sizes)), glyph_map_new, myrects)

    @staticmethod
    @functools.lru_cache(50)
    def _get_ps_font_and_encoding(texname):
        tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
        psfont = tex_font_map[texname]
        if psfont.filename is None:
            raise ValueError(
                f"No usable font file found for {psfont.psname} ({texname}). "
                f"The font may lack a Type-1 version.")

        font = get_font(psfont.filename)

        if psfont.encoding:
            # If psfonts.map specifies an encoding, use it: it gives us a
            # mapping of glyph indices to Adobe glyph names; use it to convert
            # dvi indices to glyph names and use the FreeType-synthesized
            # unicode charmap to convert glyph names to glyph indices (with
            # FT_Get_Name_Index/get_name_index), and load the glyph using
            # FT_Load_Glyph/load_glyph.  (That charmap has a coverage at least
            # as good as, and possibly better than, the native charmaps.)
            enc = dviread._parse_enc(psfont.encoding)
        else:
            # If psfonts.map specifies no encoding, the indices directly
            # map to the font's "native" charmap; so don't use the
            # FreeType-synthesized charmap but the native ones (we can't
            # directly identify it but it's typically an Adobe charmap), and
            # directly load the dvi glyph indices using FT_Load_Char/load_char.
            for charmap_code in [
                    1094992451,  # ADOBE_CUSTOM.
                    1094995778,  # ADOBE_STANDARD.
            ]:
                try:
                    font.select_charmap(charmap_code)
                except (ValueError, RuntimeError):
                    pass
                else:
                    break
            else:
                _log.warning("No supported encoding in font (%s).",
                             psfont.filename)
            enc = None

        return font, enc
class RendererKivy(RendererBase):
    '''The kivy renderer handles drawing/rendering operations. A RendererKivy
       should be initialized with a FigureCanvasKivy widget. On initialization
       a MathTextParser is instantiated to generate math text inside a
       FigureCanvasKivy widget. Additionally a list to store clip_rectangles
       is defined for elements that need to be clipped inside a rectangle such
       as axes. The rest of the render is performed using kivy graphics
       instructions.
    '''
    def __init__(self, widget):
        super(RendererKivy, self).__init__()
        self.widget = widget
        self.dpi = widget.figure.dpi
        self._markers = {}
        #  Can be enhanced by using TextToPath matplotlib, textpath.py
        self.mathtext_parser = MathTextParser("Bitmap")
        self.list_goraud_triangles = []
        self.clip_rectangles = []
        self.labels_inside_plot = []

    def contains(self, widget, x, y):
        '''Returns whether or not a point is inside the widget. The value
           of the point is defined in x, y as kivy coordinates.
        '''
        left = widget.x
        bottom = widget.y
        top = widget.y + widget.height
        right = widget.x + widget.width
        return (left <= x <= right and bottom <= y <= top)

    def handle_clip_rectangle(self, gc, x, y):
        '''It checks whether the point (x,y) collides with any already
           existent stencil. If so it returns the index position of the
           stencil it collides with. if the new clip rectangle bounds are
           None it draws in the canvas otherwise it finds the correspondent
           stencil or creates a new one for the new graphics instructions.
           The point x,y is given in matplotlib coordinates.
        '''
        x = self.widget.x + x
        y = self.widget.y + y
        collides = self.collides_with_existent_stencil(x, y)
        if collides > -1:
            return collides
        new_bounds = gc.get_clip_rectangle()
        if new_bounds:
            x = self.widget.x + int(new_bounds.bounds[0])
            y = self.widget.y + int(new_bounds.bounds[1])
            w = int(new_bounds.bounds[2])
            h = int(new_bounds.bounds[3])
            collides = self.collides_with_existent_stencil(x, y)
            if collides == -1:
                cliparea = StencilView(pos=(x, y), size=(w, h))
                self.clip_rectangles.append(cliparea)
                self.widget.add_widget(cliparea)
                return len(self.clip_rectangles) - 1
            else:
                return collides
        else:
            return -2

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):
        '''Draws a collection of paths selecting drawing properties from
           the lists *facecolors*, *edgecolors*, *linewidths*,
           *linestyles* and *antialiaseds*. *offsets* is a list of
           offsets to apply to each of the paths. The offsets in
           *offsets* are first transformed by *offsetTrans* before being
           applied.  *offset_position* may be either "screen" or "data"
           depending on the space that the offsets are in.
        '''
        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)
        # check whether an optimization is needed by calculating the cost of
        # generating and use a path with the cost of emitting a path in-line.
        should_do_optimization = \
            len_path + uses_per_path + 5 < len_path * 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)
        # Generate an array of unique paths with the respective transformations
        path_codes = []
        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)
            polygons = path.to_polygons(transform)
            path_codes.append(polygons)
        # Apply the styles and rgbFace to each one of the raw paths from
        # the list. Additionally a transformation is being applied to
        # translate each independent path
        for xo, yo, path_poly, gc0, rgbFace in self._iter_collection(
                gc, master_transform, all_transforms, path_codes, offsets,
                offsetTrans, facecolors, edgecolors, linewidths, linestyles,
                antialiaseds, urls, offset_position):
            list_canvas_instruction = self.get_path_instructions(
                gc0, path_poly, closed=True, rgbFace=rgbFace)
            for widget, instructions in list_canvas_instruction:
                widget.canvas.add(PushMatrix())
                widget.canvas.add(Translate(xo, yo))
                widget.canvas.add(instructions)
                widget.canvas.add(PopMatrix())

    def collides_with_existent_stencil(self, x, y):
        '''Check all the clipareas and returns the index of the clip area that
           contains this point. The point x, y is given in kivy coordinates.
        '''
        idx = -1
        for cliparea in self.clip_rectangles:
            idx += 1
            if self.contains(cliparea, x, y):
                return idx
        return -1

    def get_path_instructions(self, gc, polygons, closed=False, rgbFace=None):
        '''With a graphics context and a set of polygons it returns a list
           of InstructionGroups required to render the path.
        '''
        instructions_list = []
        points_line = []
        for polygon in polygons:
            for x, y in polygon:
                x = x + self.widget.x
                y = y + self.widget.y
                points_line += [
                    float(x),
                    float(y),
                ]
            tess = Tesselator()
            tess.add_contour(points_line)
            if not tess.tesselate():
                Logger.warning("Tesselator didn't work :(")
                return
            newclip = self.handle_clip_rectangle(gc, x, y)
            if newclip > -1:
                instructions_list.append((self.clip_rectangles[newclip],
                                          self.get_graphics(gc,
                                                            tess,
                                                            points_line,
                                                            rgbFace,
                                                            closed=closed)))
            else:
                instructions_list.append((self.widget,
                                          self.get_graphics(gc,
                                                            tess,
                                                            points_line,
                                                            rgbFace,
                                                            closed=closed)))
        return instructions_list

    def get_graphics(self, gc, polygons, points_line, rgbFace, closed=False):
        '''Return an instruction group which contains the necessary graphics
           instructions to draw the respective graphics.
        '''
        instruction_group = InstructionGroup()
        if isinstance(gc.line['dash_list'], tuple):
            gc.line['dash_list'] = list(gc.line['dash_list'])
        if rgbFace is not None:
            if len(polygons.meshes) != 0:
                instruction_group.add(Color(*rgbFace))
                for vertices, indices in polygons.meshes:
                    instruction_group.add(
                        Mesh(vertices=vertices,
                             indices=indices,
                             mode=str("triangle_fan")))
        instruction_group.add(Color(*gc.get_rgb()))
        if _mpl_1_5 and closed:
            points_poly_line = points_line[:-2]
        else:
            points_poly_line = points_line
        if gc.line['width'] > 0:
            instruction_group.add(
                Line(points=points_poly_line,
                     width=int(gc.line['width'] / 2),
                     dash_length=gc.line['dash_length'],
                     dash_offset=gc.line['dash_offset'],
                     dash_joint=gc.line['joint_style'],
                     dash_list=gc.line['dash_list']))
        return instruction_group

    def draw_image(self, gc, x, y, im):
        '''Render images that can be displayed on a matplotlib figure.
           These images are generally called using imshow method from pyplot.
           A Texture is applied to the FigureCanvas. The position x, y is
           given in matplotlib coordinates.
        '''
        # Clip path to define an area to mask.
        clippath, clippath_trans = gc.get_clip_path()
        # Normal coordinates calculated and image added.
        x = self.widget.x + x
        y = self.widget.y + y
        bbox = gc.get_clip_rectangle()
        if bbox is not None:
            l, b, w, h = bbox.bounds
        else:
            l = 0
            b = 0
            w = self.widget.width
            h = self.widget.height
        h, w = im.get_size_out()
        rows, cols, image_str = im.as_rgba_str()
        texture = Texture.create(size=(w, h))
        texture.blit_buffer(image_str, colorfmt='rgba', bufferfmt='ubyte')
        if clippath is None:
            with self.widget.canvas:
                Color(1.0, 1.0, 1.0, 1.0)
                Rectangle(texture=texture, pos=(x, y), size=(w, h))
        else:
            polygons = clippath.to_polygons(clippath_trans)
            list_canvas_instruction = self.get_path_instructions(
                gc, polygons, rgbFace=(1.0, 1.0, 1.0, 1.0))
            for widget, instructions in list_canvas_instruction:
                widget.canvas.add(StencilPush())
                widget.canvas.add(instructions)
                widget.canvas.add(StencilUse())
                widget.canvas.add(Color(1.0, 1.0, 1.0, 1.0))
                widget.canvas.add(
                    Rectangle(texture=texture, pos=(x, y), size=(w, h)))
                widget.canvas.add(StencilUnUse())
                widget.canvas.add(StencilPop())

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        '''Render text that is displayed in the canvas. The position x, y is
           given in matplotlib coordinates. A `GraphicsContextKivy` is given
           to render according to the text properties such as color, size, etc.
           An angle is given to change the orientation of the text when needed.
           If the text is a math expression it will be rendered using a
           MathText parser.
        '''
        if mtext:
            transform = mtext.get_transform()
            ax, ay = transform.transform_point(mtext.get_position())

            angle_rad = mtext.get_rotation() * np.pi / 180.
            dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)])

            if mtext.get_rotation_mode() == "anchor":
                # if anchor mode, rotation is undone first
                v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
                ax = ax + v_offset * dir_vert[0]
                ay = ay + v_offset * dir_vert[1]

            w, h, d = self.get_text_width_height_descent(s, prop, ismath)
            ha, va = mtext.get_ha(), mtext.get_va()
            if ha == "center":
                ax -= w / 2
            elif ha == "right":
                ax -= w
            if va == "top":
                ay -= h
            elif va == "center":
                ay -= h / 2

            if mtext.get_rotation_mode() != "anchor":
                # if not anchor mode, rotation is undone last
                v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
                ax = ax + v_offset * dir_vert[0]
                ay = ay + v_offset * dir_vert[1]

            x, y = ax, ay

        x += self.widget.x
        y += self.widget.y

        if ismath:
            self.draw_mathtext(gc, x, y, s, prop, angle)
        else:
            font = resource_find(prop.get_name() + ".ttf")
            if font is None:
                plot_text = CoreLabel(font_size=prop.get_size_in_points())
            else:
                plot_text = CoreLabel(font_size=prop.get_size_in_points(),
                                      font_name=prop.get_name())
            plot_text.text = six.text_type("{}".format(s))
            if prop.get_style() == 'italic':
                plot_text.italic = True
            if weight_as_number(prop.get_weight()) > 500:
                plot_text.bold = True
            plot_text.refresh()
            with self.widget.canvas:
                if isinstance(angle, float):
                    PushMatrix()
                    Rotate(angle=angle, origin=(int(x), int(y)))
                    Rectangle(pos=(int(x), int(y)),
                              texture=plot_text.texture,
                              size=plot_text.texture.size)
                    PopMatrix()
                else:
                    Rectangle(pos=(int(x), int(y)),
                              texture=plot_text.texture,
                              size=plot_text.texture.size)

    def draw_mathtext(self, gc, x, y, s, prop, angle):
        '''Draw the math text using matplotlib.mathtext. The position
           x,y is given in Kivy coordinates.
        '''
        ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop)
        w = ftimage.get_width()
        h = ftimage.get_height()
        texture = Texture.create(size=(w, h))
        if _mpl_1_5:
            texture.blit_buffer(ftimage.as_rgba_str()[0][0],
                                colorfmt='rgba',
                                bufferfmt='ubyte')
        else:
            texture.blit_buffer(ftimage.as_rgba_str(),
                                colorfmt='rgba',
                                bufferfmt='ubyte')
        texture.flip_vertical()
        with self.widget.canvas:
            Rectangle(texture=texture, pos=(x, y), size=(w, h))

    def draw_path(self, gc, path, transform, rgbFace=None):
        '''Produce the rendering of the graphics elements using
           :class:`kivy.graphics.Line` and :class:`kivy.graphics.Mesh` kivy
           graphics instructions. The paths are converted into polygons and
           assigned either to a clip rectangle or to the same canvas for
           rendering. Paths are received in matplotlib coordinates. The
           aesthetics is defined by the `GraphicsContextKivy` gc.
        '''
        polygons = path.to_polygons(transform, self.widget.width,
                                    self.widget.height)
        list_canvas_instruction = self.get_path_instructions(gc,
                                                             polygons,
                                                             closed=True,
                                                             rgbFace=rgbFace)
        for widget, instructions in list_canvas_instruction:
            widget.canvas.add(instructions)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        '''Markers graphics instructions are stored on a dictionary and
           hashed through graphics context and rgbFace values. If a marker_path
           with the corresponding graphics context exist then the instructions
           are pulled from the markers dictionary.
        '''
        if not len(path.vertices):
            return
        # get a string representation of the path
        path_data = self._convert_path(marker_path,
                                       marker_trans +
                                       Affine2D().scale(1.0, -1.0),
                                       simplify=False)
        # get a string representation of the graphics context and rgbFace.
        style = str(gc._get_style_dict(rgbFace))
        dictkey = (path_data, str(style))
        # check whether this marker has been created before.
        list_instructions = self._markers.get(dictkey)
        # creating a list of instructions for the specific marker.
        if list_instructions is None:
            polygons = marker_path.to_polygons(marker_trans)
            self._markers[dictkey] = self.get_path_instructions(
                gc, polygons, rgbFace=rgbFace)
        # Traversing all the positions where a marker should be rendered
        for vertices, codes in path.iter_segments(trans, simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                for widget, instructions in self._markers[dictkey]:
                    widget.canvas.add(PushMatrix())
                    widget.canvas.add(Translate(x, y))
                    widget.canvas.add(instructions)
                    widget.canvas.add(PopMatrix())

    def flipy(self):
        return False

    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
        if _mpl_1_5:
            return _path.convert_to_string(path, transform, clip, simplify,
                                           sketch, 6,
                                           [b'M', b'L', b'Q', b'C', b'z'],
                                           False).decode('ascii')
        else:
            return _path.convert_to_svg(path, transform, clip, simplify, 6)

    def get_canvas_width_height(self):
        '''Get the actual width and height of the widget.
        '''
        return self.widget.width, self.widget.height

    def get_text_width_height_descent(self, s, prop, ismath):
        '''This method is needed specifically to calculate text positioning
           in the canvas. Matplotlib needs the size to calculate the points
           according to their layout
        '''
        if ismath:
            ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop)
            w = ftimage.get_width()
            h = ftimage.get_height()
            return w, h, depth
        font = resource_find(prop.get_name() + ".ttf")
        if font is None:
            plot_text = CoreLabel(font_size=prop.get_size_in_points())
        else:
            plot_text = CoreLabel(font_size=prop.get_size_in_points(),
                                  font_name=prop.get_name())
        plot_text.text = six.text_type("{}".format(s))
        plot_text.refresh()
        return plot_text.texture.size[0], plot_text.texture.size[1], 1

    def new_gc(self):
        '''Instantiate a GraphicsContextKivy object
        '''
        return GraphicsContextKivy(self.widget)

    def points_to_pixels(self, points):
        return points / 72.0 * self.dpi
예제 #50
0
class RendererCairo(RendererBase):
    fontweights = {
        100          : cairo.FONT_WEIGHT_NORMAL,
        200          : cairo.FONT_WEIGHT_NORMAL,
        300          : cairo.FONT_WEIGHT_NORMAL,
        400          : cairo.FONT_WEIGHT_NORMAL,
        500          : cairo.FONT_WEIGHT_NORMAL,
        600          : cairo.FONT_WEIGHT_BOLD,
        700          : cairo.FONT_WEIGHT_BOLD,
        800          : cairo.FONT_WEIGHT_BOLD,
        900          : cairo.FONT_WEIGHT_BOLD,
        'ultralight' : cairo.FONT_WEIGHT_NORMAL,
        'light'      : cairo.FONT_WEIGHT_NORMAL,
        'normal'     : cairo.FONT_WEIGHT_NORMAL,
        'medium'     : cairo.FONT_WEIGHT_NORMAL,
        'semibold'   : cairo.FONT_WEIGHT_BOLD,
        'bold'       : cairo.FONT_WEIGHT_BOLD,
        'heavy'      : cairo.FONT_WEIGHT_BOLD,
        'ultrabold'  : cairo.FONT_WEIGHT_BOLD,
        'black'      : cairo.FONT_WEIGHT_BOLD,
                   }
    fontangles = {
        'italic'  : cairo.FONT_SLANT_ITALIC,
        'normal'  : cairo.FONT_SLANT_NORMAL,
        'oblique' : cairo.FONT_SLANT_OBLIQUE,
        }


    def __init__(self, dpi):
        """
        """
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.dpi = dpi
        self.gc = GraphicsContextCairo (renderer=self)
        self.text_ctx = cairo.Context (
           cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1))
        self.mathtext_parser = MathTextParser('Cairo')

        RendererBase.__init__(self)

    def set_ctx_from_surface (self, surface):
        self.gc.ctx = cairo.Context (surface)


    def set_width_height(self, width, height):
        self.width  = width
        self.height = height
        self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height)
        # use matrix_flipy for ALL rendering?
        # - problem with text? - will need to switch matrix_flipy off, or do a
        # font transform?


    def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides):
        if fill_c is not None:
            ctx.save()
            if len(fill_c) == 3 or alpha_overrides:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha)
            else:
                ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], fill_c[3])
            ctx.fill_preserve()
            ctx.restore()
        ctx.stroke()

    @staticmethod
    def convert_path(ctx, path, transform, clip=None):
        for points, code in path.iter_segments(transform, clip=clip):
            if code == Path.MOVETO:
                ctx.move_to(*points)
            elif code == Path.CLOSEPOLY:
                ctx.close_path()
            elif code == Path.LINETO:
                ctx.line_to(*points)
            elif code == Path.CURVE3:
                ctx.curve_to(points[0], points[1],
                             points[0], points[1],
                             points[2], points[3])
            elif code == Path.CURVE4:
                ctx.curve_to(*points)


    def draw_path(self, gc, path, transform, rgbFace=None):
        ctx = gc.ctx

        # We'll clip the path to the actual rendering extents
        # if the path isn't filled.
        if rgbFace is None and gc.get_hatch() is None:
            clip = ctx.clip_extents()
        else:
            clip = None

        transform = transform + \
            Affine2D().scale(1.0, -1.0).translate(0, self.height)

        ctx.new_path()
        self.convert_path(ctx, path, transform, clip)

        self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())

    def draw_image(self, gc, x, y, im):
        # bbox - not currently used
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        if sys.byteorder == 'little':
            im = im[:, :, (2, 1, 0, 3)]
        else:
            im = im[:, :, (3, 0, 1, 2)]
        surface = cairo.ImageSurface.create_for_data(
            memoryview(im.flatten()), cairo.FORMAT_ARGB32, im.shape[1], im.shape[0],
            im.shape[1]*4)
        ctx = gc.ctx
        y = self.height - y - im.shape[0]

        ctx.save()
        ctx.set_source_surface(surface, float(x), float(y))
        if gc.get_alpha() != 1.0:
            ctx.paint_with_alpha(gc.get_alpha())
        else:
            ctx.paint()
        ctx.restore()

    def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
        # Note: x,y are device/display coords, not user-coords, unlike other
        # draw_* methods
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

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

        else:
            ctx = gc.ctx
            ctx.new_path()
            ctx.move_to (x, y)
            ctx.select_font_face (prop.get_name(),
                                  self.fontangles [prop.get_style()],
                                  self.fontweights[prop.get_weight()])

            size = prop.get_size_in_points() * self.dpi / 72.0

            ctx.save()
            if angle:
                ctx.rotate (-angle * np.pi / 180)
            ctx.set_font_size (size)

            if HAS_CAIRO_CFFI:
                if not isinstance(s, six.text_type):
                    s = six.text_type(s)
            else:
                if not six.PY3 and isinstance(s, six.text_type):
                    s = s.encode("utf-8")

            ctx.show_text(s)
            ctx.restore()

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))

        ctx = gc.ctx
        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.dpi, prop)

        ctx.save()
        ctx.translate(x, y)
        if angle:
            ctx.rotate (-angle * np.pi / 180)

        for font, fontsize, s, ox, oy in glyphs:
            ctx.new_path()
            ctx.move_to(ox, oy)

            fontProp = ttfFontProperty(font)
            ctx.save()
            ctx.select_font_face (fontProp.name,
                                  self.fontangles [fontProp.style],
                                  self.fontweights[fontProp.weight])

            size = fontsize * self.dpi / 72.0
            ctx.set_font_size(size)
            if not six.PY3 and isinstance(s, six.text_type):
                s = s.encode("utf-8")
            ctx.show_text(s)
            ctx.restore()

        for ox, oy, w, h in rects:
            ctx.new_path()
            ctx.rectangle (ox, oy, w, h)
            ctx.set_source_rgb (0, 0, 0)
            ctx.fill_preserve()

        ctx.restore()


    def flipy(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return True
        #return False # tried - all draw objects ok except text (and images?)
        # which comes out mirrored!


    def get_canvas_width_height(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return self.width, self.height


    def get_text_width_height_descent(self, s, prop, ismath):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        if ismath:
            width, height, descent, fonts, used_characters = self.mathtext_parser.parse(
               s, self.dpi, prop)
            return width, height, descent

        ctx = self.text_ctx
        ctx.save()
        ctx.select_font_face (prop.get_name(),
                              self.fontangles [prop.get_style()],
                              self.fontweights[prop.get_weight()])

        # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
        # but if /96.0 is used the font is too small

        size = prop.get_size_in_points() * self.dpi / 72.0

        # problem - scale remembers last setting and font can become
        # enormous causing program to crash
        # save/restore prevents the problem
        ctx.set_font_size (size)

        y_bearing, w, h = ctx.text_extents (s)[1:4]
        ctx.restore()

        return w, h, h + y_bearing


    def new_gc(self):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        self.gc.ctx.save()
        self.gc._alpha = 1.0
        self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
        return self.gc


    def points_to_pixels(self, points):
        if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
        return points/72.0 * self.dpi
예제 #51
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()
예제 #52
0
class TextToPath(object):
    """
    A class that convert a given text to a path using ttf fonts.
    """

    FONT_SCALE = 50.
    DPI = 72

    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

    def _get_font(self, prop):
        """
        find a ttf font.
        """
        fname = font_manager.findfont(prop)
        font = FT2Font(str(fname))
        font.set_size(self.FONT_SCALE, self.DPI)

        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        ps_name = font.get_sfnt()[(1,0,0,6)]
        char_id = urllib.quote('%s-%d' % (ps_name, ccode))
        return char_id

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib.quote('%s-%d' % (ps_name, ccode))
        return char_id


    def glyph_to_path(self, glyph, currx=0.):
        """
        convert the ft2font glyph to vertices and codes.
        """
        #Mostly copied from backend_svg.py.

        verts, codes = [], []
        for step in glyph.path:
            if step[0] == 0:   # MOVE_TO
                verts.append((step[1], step[2]))
                codes.append(Path.MOVETO)
            elif step[0] == 1: # LINE_TO
                verts.append((step[1], step[2]))
                codes.append(Path.LINETO)
            elif step[0] == 2: # CURVE3
                verts.extend([(step[1], step[2]),
                               (step[3], step[4])])
                codes.extend([Path.CURVE3, Path.CURVE3])
            elif step[0] == 3: # CURVE4
                verts.extend([(step[1], step[2]),
                              (step[3], step[4]),
                              (step[5], step[6])])
                codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
            elif step[0] == 4: # ENDPOLY
                verts.append((0, 0,))
                codes.append(Path.CLOSEPOLY)

        verts = [(x+currx, y) for (x,y) in verts]
        return verts, codes


    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        convert text *s* to path (a tuple of vertices and codes for matplotlib.math.Path).

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. Effective only if usetex == False.


        """
        if usetex==False:
            if ismath == False:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if verts1:
                verts1 = np.array(verts1)*scale + [xposition, yposition]
            verts.extend(verts1)
            codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes


    def get_glyphs_with_font(self, font, s, glyph_map=None,
                             return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using the
        provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        cmap = font.get_charmap()
        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:


            ccode = ord(c)
            gind = cmap.get(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0


            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = (glyph.linearHoriAdvance / 65536.0)

            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                glyph_map_new[char_id] = self.glyph_to_path(glyph)

            currx += (kern / 64.0)

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, rects




    def get_glyphs_mathtext(self, prop, s, glyph_map=None,
                            return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes by parsing it with mathtext.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)


        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        currx, curry = 0, 0
        for font, fontsize, s, ox, oy in glyphs:

            ccode = ord(s)
            char_id = self._get_char_id(font, ccode)
            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = self.glyph_to_path(glyph)

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1=[(ox, oy), (ox, oy+h), (ox+w, oy+h), (ox+w, oy), (ox, oy), (0,0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))


        return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map, myrects


    def get_texmanager(self):
        """
        return the :class:`matplotlib.texmanager.TexManager` instance
        """
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager


    def get_glyphs_tex(self, prop, s, glyph_map=None,
                       return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using matplotlib's usetex mode.
        """

        # codes are modstly borrowed from pdf backend.

        texmanager = self.get_texmanager()

        if self.tex_font_map is None:
            self.tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))

        fontsize = prop.get_size_in_points()
        if hasattr(texmanager, "get_dvi"): #
            dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
            dvi = dviread.DviFromFileLike(dvifilelike, self.DPI)
        else:
            dvifile = texmanager.make_dvi(s, self.FONT_SCALE)
            dvi = dviread.Dvi(dvifile, self.DPI)
        page = iter(dvi).next()
        dvi.close()


        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map


        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        #oldfont, seq = None, []
        for x1, y1, dvifont, glyph, width in page.text:
            font_and_encoding = self._ps_fontd.get(dvifont.texname)

            if font_and_encoding is None:
                font_bunch =  self.tex_font_map[dvifont.texname]
                font = FT2Font(font_bunch.filename)
                try:
                    font.select_charmap(1094992451) # select ADOBE_CUSTOM
                except ValueError:
                    font.set_charmap(0)
                if font_bunch.encoding:
                    enc = dviread.Encoding(font_bunch.encoding)
                else:
                    enc = None
                self._ps_fontd[dvifont.texname] = font, enc

            else:
                font, enc = font_and_encoding

            ft2font_flag = LOAD_TARGET_LIGHT

            char_id = self._get_char_id_ps(font, glyph)

            if not char_id in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)

                glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = self.glyph_to_path(glyph0)

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size/self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1=[(ox, oy), (ox+w, oy), (ox+w, oy+h), (ox, oy+h), (ox, oy), (0,0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))


        return zip(glyph_ids, xpositions, ypositions, sizes), \
               glyph_map_new, myrects
예제 #53
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(
            '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)

    def _write_default_style(self):
        writer = self.writer
        default_style = generate_css({
            'stroke-linejoin': 'round',
            'stroke-linecap': 'square'
        })
        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):
        return '%s%s' % (type, md5(str(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
        """
        dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb())
        oid = self._hatchd.get(dictkey)
        if oid is None:
            oid = self._make_id('h', dictkey)
            self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace,
                                      gc.get_rgb()), 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('pattern',
                         id=oid,
                         patternUnits="userSpaceOnUse",
                         x="0",
                         y="0",
                         width=str(HATCH_SIZE),
                         height=str(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=str(HATCH_SIZE + 1),
                           height=str(HATCH_SIZE + 1),
                           fill=fill)
            writer.element('path',
                           d=path_data,
                           style=generate_css({
                               'fill': rgb2hex(stroke),
                               'stroke': rgb2hex(stroke),
                               'stroke-width': str(1.0),
                               '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 = {}

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

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

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

        linewidth = gc.get_linewidth()
        if linewidth:
            attrib['stroke'] = rgb2hex(gc.get_rgb())
            if linewidth != 1.0:
                attrib['stroke-width'] = str(linewidth)
            if gc.get_joinstyle() != 'round':
                attrib['stroke-linejoin'] = gc.get_joinstyle()
            if gc.get_capstyle() != 'projecting':
                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 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('path', d=path_data)
            else:
                x, y, w, h = clip
                writer.element('rect',
                               x=str(x),
                               y=str(y),
                               width=str(w),
                               height=str(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 self._fonts.items():
            font = FT2Font(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(str(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': str(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):
        """
        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['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 style.keys():
            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}
        for vertices, code in path.iter_segments(trans_and_flip,
                                                 simplify=False):
            if len(vertices):
                x, y = vertices[-2:]
                attrib['x'] = str(x)
                attrib['y'] = str(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):
        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, path_codes, offsets, offsetTrans, facecolors, edgecolors,
                linewidths, linestyles, antialiaseds, urls):
            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': str(xo),
                'y': str(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 = 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

            writer.start('linearGradient',
                         id="GR%x_%d" % (self._n_gradients, i),
                         x1=str(x1),
                         y1=str(y1),
                         x2=str(xb),
                         y2=str(yb))
            writer.element('stop',
                           offset='0',
                           style=generate_css({
                               'stop-color': rgb2hex(c),
                               'stop-opacity': str(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(
                           [str(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': str(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 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('g', attrib={'clip-path': '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['transform'] = generate_transform([('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()

        url = getattr(im, '_url', None)
        if url is not None:
            self.writer.start('a', attrib={'xlink:href': url})
        if rcParams['svg.image_inline']:
            stringio = cStringIO.StringIO()
            im.flipud_out()
            rows, cols, buffer = im.as_rgba_str()
            _png.write_png(buffer, cols, rows, stringio)
            im.flipud_out()
            attrib['xlink:href'] = ("data:image/png;base64,\n" +
                                    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()
            attrib['xlink:href'] = filename

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

        if transform is None:
            self.writer.element('image',
                                x=str(x / trans[0]),
                                y=str((self.height - y) / trans[3] - h),
                                width=str(w),
                                height=str(h),
                                attrib=attrib)
        else:
            flipped = self._make_flip_transform(transform)
            attrib['transform'] = generate_transform([('matrix',
                                                       flipped.to_values())])
            self.writer.element('image',
                                x=str(x),
                                y=str(y + dy),
                                width=str(dx),
                                height=str(-dy),
                                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):
        """
        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'] = str(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('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('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'] = str(xposition)
                if yposition != 0.0:
                    attrib['y'] = str(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 glyph_map_new.iteritems():
                    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):
        writer = self.writer

        color = rgb2hex(gc.get_rgb())
        style = {}
        if color != '#000000':
            style['fill'] = color
        if gc.get_alpha() != 1.0:
            style['opacity'] = str(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['font-size'] = str(fontsize) + 'px'
            style['font-family'] = str(fontfamily)
            style['font-style'] = prop.get_style().lower()
            attrib['style'] = generate_css(style)

            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 = {}
            for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
                style = generate_css({
                    # Must add "px" to work around a Firefox bug
                    'font-size': str(fontsize) + 'px',
                    'font-family': font.family_name,
                    '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 = str(chars[0][1])
                else:
                    ys = ' '.join(str(c[1]) for c in chars)

                attrib = {
                    'style': style,
                    'x': ' '.join(str(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=str(x),
                                   y=str(-y + height),
                                   width=str(width),
                                   height=str(height))

            writer.end('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('g', attrib={'clip-path': '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('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)
예제 #54
0
class TextToPath(object):
    """
    A class that convert a given text to a path using ttf fonts.
    """

    FONT_SCALE = 100.
    DPI = 72

    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

        self._adobe_standard_encoding = None

    def _get_adobe_standard_encoding(self):
        enc_name = dviread.find_tex_file('8a.enc')
        enc = dviread.Encoding(enc_name)
        return dict([(c, i) for i, c in enumerate(enc.encoding)])

    def _get_font(self, prop):
        """
        find a ttf font.
        """
        fname = font_manager.findfont(prop)
        font = get_font(fname)
        font.set_size(self.FONT_SCALE, self.DPI)

        return font

    def _get_hinting_flag(self):
        return LOAD_NO_HINTING

    def _get_char_id(self, font, ccode):
        """
        Return a unique id for the given font and character-code set.
        """
        sfnt = font.get_sfnt()
        try:
            ps_name = sfnt[(1, 0, 0, 6)].decode('macroman')
        except KeyError:
            ps_name = sfnt[(3, 1, 0x0409, 6)].decode('utf-16be')
        char_id = urllib_quote('%s-%x' % (ps_name, ccode))
        return char_id

    def _get_char_id_ps(self, font, ccode):
        """
        Return a unique id for the given font and character-code set (for tex).
        """
        ps_name = font.get_ps_font_info()[2]
        char_id = urllib_quote('%s-%d' % (ps_name, ccode))
        return char_id

    def glyph_to_path(self, font, currx=0.):
        """
        convert the ft2font glyph to vertices and codes.
        """
        verts, codes = font.get_path()
        if currx != 0.0:
            verts[:, 0] += currx
        return verts, codes

    def get_text_width_height_descent(self, s, prop, ismath):
        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=None)
            return w, h, d

        fontsize = prop.get_size_in_points()
        scale = float(fontsize) / self.FONT_SCALE

        if ismath:
            prop = prop.copy()
            prop.set_size(self.FONT_SCALE)

            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width * scale, height * scale, descent * scale

        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 * scale, h * scale, d * scale

    def get_text_path(self, prop, s, ismath=False, usetex=False):
        """
        convert text *s* to path (a tuple of vertices and codes for
        matplotlib.path.Path).

        *prop*
          font property

        *s*
          text to be converted

        *usetex*
          If True, use matplotlib usetex mode.

        *ismath*
          If True, use mathtext parser. Effective only if usetex == False.


        """
        if not usetex:
            if not ismath:
                font = self._get_font(prop)
                glyph_info, glyph_map, rects = self.get_glyphs_with_font(
                                                    font, s)
            else:
                glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
                                                    prop, s)
        else:
            glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)

        verts, codes = [], []

        for glyph_id, xposition, yposition, scale in glyph_info:
            verts1, codes1 = glyph_map[glyph_id]
            if len(verts1):
                verts1 = np.array(verts1) * scale + [xposition, yposition]
                verts.extend(verts1)
                codes.extend(codes1)

        for verts1, codes1 in rects:
            verts.extend(verts1)
            codes.extend(codes1)

        return verts, codes

    def get_glyphs_with_font(self, font, s, glyph_map=None,
                             return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using the
        provided ttf font.
        """

        # Mostly copied from backend_svg.py.

        lastgind = None

        currx = 0
        xpositions = []
        glyph_ids = []

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = OrderedDict()
        else:
            glyph_map_new = glyph_map

        # I'm not sure if I get kernings right. Needs to be verified. -JJL

        for c in s:
            ccode = ord(c)
            gind = font.get_char_index(ccode)
            if gind is None:
                ccode = ord('?')
                gind = 0

            if lastgind is not None:
                kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
            else:
                kern = 0

            glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
            horiz_advance = (glyph.linearHoriAdvance / 65536.0)

            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                glyph_map_new[char_id] = self.glyph_to_path(font)

            currx += (kern / 64.0)

            xpositions.append(currx)
            glyph_ids.append(char_id)

            currx += horiz_advance

            lastgind = gind

        ypositions = [0] * len(xpositions)
        sizes = [1.] * len(xpositions)

        rects = []

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                     glyph_map_new, rects)

    def get_glyphs_mathtext(self, prop, s, glyph_map=None,
                            return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes by parsing it with
        mathtext.
        """

        prop = prop.copy()
        prop.set_size(self.FONT_SCALE)

        width, height, descent, glyphs, rects = self.mathtext_parser.parse(
            s, self.DPI, prop)

        if not glyph_map:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        xpositions = []
        ypositions = []
        glyph_ids = []
        sizes = []

        currx, curry = 0, 0
        for font, fontsize, ccode, ox, oy in glyphs:
            char_id = self._get_char_id(font, ccode)
            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
                glyph_map_new[char_id] = self.glyph_to_path(font)

            xpositions.append(ox)
            ypositions.append(oy)
            glyph_ids.append(char_id)
            size = fontsize / self.FONT_SCALE
            sizes.append(size)

        myrects = []
        for ox, oy, w, h in rects:
            vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
                     (ox + w, oy), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)

    def get_texmanager(self):
        """
        return the :class:`matplotlib.texmanager.TexManager` instance
        """
        if self._texmanager is None:
            from matplotlib.texmanager import TexManager
            self._texmanager = TexManager()
        return self._texmanager

    def get_glyphs_tex(self, prop, s, glyph_map=None,
                       return_new_glyphs_only=False):
        """
        convert the string *s* to vertices and codes using matplotlib's usetex
        mode.
        """

        # codes are modstly borrowed from pdf backend.

        texmanager = self.get_texmanager()

        if self.tex_font_map is None:
            self.tex_font_map = dviread.PsfontsMap(
                                    dviread.find_tex_file('pdftex.map'))

        if self._adobe_standard_encoding is None:
            self._adobe_standard_encoding = self._get_adobe_standard_encoding()

        fontsize = prop.get_size_in_points()
        if hasattr(texmanager, "get_dvi"):
            dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
            dvi = dviread.DviFromFileLike(dvifilelike, self.DPI)
        else:
            dvifile = texmanager.make_dvi(s, self.FONT_SCALE)
            dvi = dviread.Dvi(dvifile, self.DPI)
        with dvi:
            page = next(iter(dvi))

        if glyph_map is None:
            glyph_map = dict()

        if return_new_glyphs_only:
            glyph_map_new = dict()
        else:
            glyph_map_new = glyph_map

        glyph_ids, xpositions, ypositions, sizes = [], [], [], []

        # Gather font information and do some setup for combining
        # characters into strings.
        # oldfont, seq = None, []
        for x1, y1, dvifont, glyph, width in page.text:
            font_and_encoding = self._ps_fontd.get(dvifont.texname)
            font_bunch = self.tex_font_map[dvifont.texname]

            if font_and_encoding is None:
                font = get_font(font_bunch.filename)

                for charmap_name, charmap_code in [("ADOBE_CUSTOM",
                                                    1094992451),
                                                   ("ADOBE_STANDARD",
                                                    1094995778)]:
                    try:
                        font.select_charmap(charmap_code)
                    except (ValueError, RuntimeError):
                        pass
                    else:
                        break
                else:
                    charmap_name = ""
                    warnings.warn("No supported encoding in font (%s)." %
                                  font_bunch.filename)

                if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
                    enc0 = dviread.Encoding(font_bunch.encoding)
                    enc = dict([(i, self._adobe_standard_encoding.get(c, None))
                                for i, c in enumerate(enc0.encoding)])
                else:
                    enc = dict()
                self._ps_fontd[dvifont.texname] = font, enc

            else:
                font, enc = font_and_encoding

            ft2font_flag = LOAD_TARGET_LIGHT

            char_id = self._get_char_id_ps(font, glyph)

            if char_id not in glyph_map:
                font.clear()
                font.set_size(self.FONT_SCALE, self.DPI)
                if enc:
                    charcode = enc.get(glyph, None)
                else:
                    charcode = glyph

                if charcode is not None:
                    glyph0 = font.load_char(charcode, flags=ft2font_flag)
                else:
                    warnings.warn("The glyph (%d) of font (%s) cannot be "
                                  "converted with the encoding. Glyph may "
                                  "be wrong" % (glyph, font_bunch.filename))

                    glyph0 = font.load_char(glyph, flags=ft2font_flag)

                glyph_map_new[char_id] = self.glyph_to_path(font)

            glyph_ids.append(char_id)
            xpositions.append(x1)
            ypositions.append(y1)
            sizes.append(dvifont.size / self.FONT_SCALE)

        myrects = []

        for ox, oy, h, w in page.boxes:
            vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
                     (ox, oy + h), (ox, oy), (0, 0)]
            code1 = [Path.MOVETO,
                     Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
                     Path.CLOSEPOLY]
            myrects.append((vert1, code1))

        return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
                glyph_map_new, myrects)
예제 #55
0
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()
    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 = np.round(x + ox + xd)
        y = np.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')

        fname = findfont(prop)
        font = get_font(
            fname,
            hinting_factor=rcParams['text.hinting_factor'])

        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 doesn't support arbitrary scaling of image.
        """
        return False

    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

        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)
            gc = self.new_gc()
            if img.dtype.kind == 'f':
                img = np.asarray(img * 255., np.uint8)
            img = img[::-1]
            self._renderer.draw_image(
                gc, l + ox, height - b - h + oy, img)
예제 #56
0
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.texd = maxdict(50)  # a cache of tex image rasters

        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 * np.sin(np.deg2rad(angle))
        yd = descent * np.cos(np.deg2rad(angle))
        x = np.round(x + ox + xd)
        y = np.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 * np.sin(np.deg2rad(angle))
        yd = d * np.cos(np.deg2rad(angle))

        #print x, y, int(x), int(y), s
        self._renderer.draw_text_image(
            font, np.round(x - xd + xo), np.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()
        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 = np.array(Z * 255.0, np.uint8)

        w, h, d = self.get_text_width_height_descent(s, prop, ismath)
        xd = d * np.sin(np.deg2rad(angle))
        yd = d * np.cos(np.deg2rad(angle))
        x = np.round(x + xd)
        y = np.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

            self._renderer.restore_region(region, x1, y1, x2, y2, ox, 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)
예제 #57
0
class RendererSVG(RendererBase):
    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  # actual dpi at which we rasterize stuff

        self._groupd = {}
        self.basename = basename
        self._image_counter = itertools.count()
        self._clipd = OrderedDict()
        self._markers = {}
        self._path_collection_id = 0
        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()
        str_height = short_float_fmt(height)
        str_width = short_float_fmt(width)
        svgwriter.write(svgProlog)
        self._start_id = self.writer.start(
            'svg',
            width='%spt' % str_width,
            height='%spt' % str_height,
            viewBox='0 0 %s %s' % (str_width, str_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.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):
        salt = mpl.rcParams['svg.hashsalt']
        if salt is None:
            salt = str(uuid.uuid4())
        m = hashlib.md5()
        m.update(salt.encode('utf8'))
        m.update(str(content).encode('utf8'))
        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_hatch_color()
        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 self._hatchd.values():
            writer.start(
                'pattern',
                id=oid,
                patternUnits="userSpaceOnUse",
                x="0", y="0", width=str(HATCH_SIZE),
                height=str(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=str(HATCH_SIZE+1),
                height=str(HATCH_SIZE+1),
                fill=fill)
            hatch_style = {
                    'fill': rgb2hex(stroke),
                    'stroke': rgb2hex(stroke),
                    'stroke-width': str(mpl.rcParams['hatch.linewidth']),
                    'stroke-linecap': 'butt',
                    'stroke-linejoin': 'miter'
                    }
            if stroke[3] < 1:
                hatch_style['stroke-opacity'] = str(stroke[3])
            writer.element(
                'path',
                d=path_data,
                style=generate_css(hatch_style)
                )
            writer.end('pattern')
        writer.end('defs')

    def _get_style_dict(self, gc, rgbFace):
        """Generate a style string 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 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('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 open_group(self, s, gid=None):
        # docstring inherited
        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):
        # docstring inherited
        self.writer.end('g')

    def option_image_nocomposite(self):
        # docstring inherited
        return not mpl.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):
        # docstring inherited
        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):
        # docstring inherited

        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)
        style = generate_css({k: v for k, v in style.items()
                              if k.startswith('stroke')})

        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):
        # docstring inherited

        # 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')
            # feColorMatrix filter to correct opacity
            writer.start(
                'filter',
                id='colorMat')
            writer.element(
                'feColorMatrix',
                attrib={'type': 'matrix'},
                values='1 0 0 0 0 \n0 1 0 0 0 \n0 0 1 0 0' +
                       ' \n1 1 1 1 0 \n0 0 0 0 1 ')
            writer.end('filter')

        avg_color = np.average(colors, axis=0)
        if avg_color[-1] == 0:
            # Skip fully-transparent triangles
            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]
            rgba_color = 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),
                gradientUnits="userSpaceOnUse",
                x1=short_float_fmt(x1), y1=short_float_fmt(y1),
                x2=short_float_fmt(xb), y2=short_float_fmt(yb))
            writer.element(
                'stop',
                offset='1',
                style=generate_css({
                    'stop-color': rgb2hex(avg_color),
                    'stop-opacity': short_float_fmt(rgba_color[-1])}))
            writer.element(
                'stop',
                offset='0',
                style=generate_css({'stop-color': rgb2hex(rgba_color),
                                    'stop-opacity': "0"}))

            writer.end('linearGradient')

        writer.end('defs')

        # triangle formation using "path"
        dpath = "M " + short_float_fmt(x1)+',' + short_float_fmt(y1)
        dpath += " L " + short_float_fmt(x2) + ',' + short_float_fmt(y2)
        dpath += " " + short_float_fmt(x3) + ',' + short_float_fmt(y3) + " Z"

        writer.element(
            'path',
            attrib={'d': dpath,
                    'fill': rgb2hex(avg_color),
                    'fill-opacity': '1',
                    'shape-rendering': "crispEdges"})

        writer.start(
                'g',
                attrib={'stroke': "none",
                        'stroke-width': "0",
                        'shape-rendering': "crispEdges",
                        'filter': "url(#colorMat)"})

        writer.element(
            'path',
            attrib={'d': dpath,
                    'fill': 'url(#GR%x_0)' % self._n_gradients,
                    'shape-rendering': "crispEdges"})

        writer.element(
            'path',
            attrib={'d': dpath,
                    'fill': 'url(#GR%x_1)' % self._n_gradients,
                    'filter': 'url(#colorAdd)',
                    'shape-rendering': "crispEdges"})

        writer.element(
            'path',
            attrib={'d': dpath,
                    'fill': 'url(#GR%x_2)' % self._n_gradients,
                    'filter': 'url(#colorAdd)',
                    'shape-rendering': "crispEdges"})

        writer.end('g')

        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):
        # docstring inherited
        return True

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

    def draw_image(self, gc, x, y, im, transform=None):
        # docstring inherited

        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 mpl.rcParams['svg.image_inline']:
            buf = BytesIO()
            Image.fromarray(im).save(buf, format="png")
            oid = oid or self._make_id('image', buf.getvalue())
            attrib['xlink:href'] = (
                "data:image/png;base64,\n" +
                base64.b64encode(buf.getvalue()).decode('ascii'))
        else:
            if self.basename is None:
                raise ValueError("Cannot save image data to filesystem when "
                                 "writing SVG to an in-memory buffer")
            filename = '{}.image{}.png'.format(
                self.basename, next(self._image_counter))
            _log.info('Writing image file for inclusion: %s', filename)
            Image.fromarray(im).save(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 _update_glyph_map_defs(self, glyph_map_new):
        """
        Emit definitions for not-yet-defined glyphs, and record them as having
        been defined.
        """
        writer = self.writer
        if glyph_map_new:
            writer.start('defs')
            for char_id, (vertices, codes) in glyph_map_new.items():
                char_id = self._adjust_char_id(char_id)
                path_data = self._convert_path(
                    Path(vertices, codes), simplify=False)
                writer.element('path', id=char_id, d=path_data)
            writer.end('defs')
            self._glyph_map.update(glyph_map_new)

    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 the textpath module.

        Parameters
        ----------
        s : str
          text to be converted
        prop : `matplotlib.font_manager.FontProperties`
          font property
        ismath : bool
          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
        alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
        if alpha != 1:
            style['opacity'] = short_float_fmt(alpha)
        font_scale = fontsize / text2path.FONT_SCALE
        attrib = {
            'style': generate_css(style),
            'transform': generate_transform([
                ('translate', (x, y)),
                ('rotate', (-angle,)),
                ('scale', (font_scale, -font_scale))]),
        }
        writer.start('g', attrib=attrib)

        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
            self._update_glyph_map_defs(glyph_map_new)

            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)

        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
            self._update_glyph_map_defs(glyph_map_new)

            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

        alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
        if alpha != 1:
            style['opacity'] = short_float_fmt(alpha)

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

            attrib = {}
            style['font-family'] = str(font.family_name)
            style['font-weight'] = str(prop.get_weight()).lower()
            style['font-stretch'] = str(prop.get_stretch()).lower()
            style['font-style'] = prop.get_style().lower()
            # Must add "px" to workaround a Firefox bug
            style['font-size'] = short_float_fmt(prop.get_size()) + 'px'
            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(mtext.get_unitless_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 = np.deg2rad(angle)
                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)

        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))

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

                if len({y for x, y, t in chars}) == 1:  # Are all y's the same?
                    ys = str(chars[0][1])
                else:
                    ys = ' '.join(str(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(chr(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):
        # docstring inherited
        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):
        # docstring inherited

        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 mpl.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):
        # docstring inherited
        return True

    def get_canvas_width_height(self):
        # docstring inherited
        return self.width, self.height

    def get_text_width_height_descent(self, s, prop, ismath):
        # docstring inherited
        return self._text2path.get_text_width_height_descent(s, prop, ismath)
예제 #58
0
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
예제 #59
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