Beispiel #1
0
class TeX1():
  """
  Render TeX code with matplotlib mathtext. Doesn't need a LaTeX installation.
  @enocde ........ Enocde base64
  @init_render ... Render the PNG image when creating an instance.
  """
  def __init__(self, src, encode = False, cfg = fs()):

    self.cfg = copy(cfg)
    self.src = src
    self.encode = encode
    self.png = None

    if self.cfg.initrender:
      self.render(self.cfg)

  def render(self, cfg):
    self.mtp = MathTextParser('bitmap')
    f = StringIO()
    self.mtp.to_png(f, self.src, cfg.forecolor, cfg.resolution, cfg.fontsize)
    bin_data = f.getvalue()
    if self.encode:
      bin_data = encodestring(bin_data)
    self.png = bin_data
    f.close()

  def _repr_png_(self):
    return self.png
Beispiel #2
0
class TeX1():
  """
  Render TeX code with matplotlib mathtext. Doesn't need a LaTeX installation.
  @texstr ........ TeX code as string
  @color ......... Font color
  @dpi ........... Resolution (dots per inch)
  @fontsize ...... Font size
  @enocde ........ Enocde base64
  @init_render ... Render the PNG image when creating an instance.
                   If 'False' one has to call the render method explicitly.
  """
  def __init__(self, texstr, color = 'black', dpi = 120, fontsize = 12,
    encode=False, init_render = True):

    self.texstr = texstr
    self.color = color
    self.dpi = dpi
    self.fontsize = fontsize
    self.encode = encode
    self.init_render = init_render
    self.png = None

    if self.init_render:
      self.render()

  def render(self):
    self.mtp = MathTextParser('bitmap')
    f = StringIO()
    self.mtp.to_png(f, self.texstr, self.color, self.dpi, self.fontsize)
    bin_data = f.getvalue()
    if self.encode:
      bin_data = encodestring(bin_data)
    self.png = bin_data
    f.close()

  def set_texstr(self, texstr):
    self.texstr = texstr
    self.render()

  def set_color(self, color):
    self.color = color
    self.render()

  def set_dpi(self, dpi):
    self.dpi = dpi
    self.render()

  def set_fontsize(self, fontsize):
    self.fontsize = fontsize
    self.render()

  def set_encode(self, encode):
    self.encode = encode
    self.render()

  def _repr_png_(self):
    return self.png
Beispiel #3
0
 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')
Beispiel #4
0
    def __init__(self, width, height, pswriter, dpi=72):
        RendererBase.__init__(self)
        self.width = width
        self.height = height
        self._pswriter = pswriter
        if rcParams['text.usetex']:
            self.textcnt = 0
            self.psfrag = []
        self.dpi = dpi

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

        self.used_characters = {}
        self.mathtext_parser = MathTextParser("PS")
Beispiel #5
0
    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")
Beispiel #6
0
    def __init__(self, width, height, dpi):
        if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
        RendererBase.__init__(self)
        self.texd = maxdict(50)  # a cache of tex image rasters
        self._fontd = maxdict(50)

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

        self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
        if __debug__: verbose.report('RendererAgg.__init__ done',
                                     'debug-annoying')
Beispiel #7
0
    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()
Beispiel #8
0
    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 __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)
Beispiel #10
0
 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")
Beispiel #11
0
 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')
Beispiel #12
0
 def render(self, cfg):
   self.mtp = MathTextParser('bitmap')
   f = StringIO()
   self.mtp.to_png(f, self.src, cfg.forecolor, cfg.resolution, cfg.fontsize)
   bin_data = f.getvalue()
   if self.encode:
     bin_data = encodestring(bin_data)
   self.png = bin_data
   f.close()
Beispiel #13
0
 def __init__(self):
     """
     Initialization
     """
     self.mathtext_parser = MathTextParser('path')
     self.tex_font_map = None
     from matplotlib.cbook import maxdict
     self._ps_fontd = maxdict(50)
     self._texmanager = None
 def render(self):
   self.mtp = MathTextParser('bitmap')
   f = StringIO()
   self.mtp.to_png(f, self.texstr, self.color, self.dpi, self.fontsize)
   bin_data = f.getvalue()
   if self.encode:
     bin_data = encodestring(bin_data)
   self.png = bin_data
   f.close()
Beispiel #15
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
Beispiel #16
0
    def __init__(self):
        self.mathtext_parser = MathTextParser('path')
        self.tex_font_map = None

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

        self._texmanager = None

        self._adobe_standard_encoding = None
 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 = []
Beispiel #18
0
    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)
Beispiel #19
0
    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)
Beispiel #20
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')
        svgwriter.write(svgProlog%(width,height,width,height))
Beispiel #21
0
 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()
Beispiel #22
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._markers = {}
        self._path_collection_id = 0
        self._imaged = {}
        self._hatchd = {}
        self.mathtext_parser = MathTextParser("SVG")
        svgwriter.write(svgProlog % (width, height, width, height))
Beispiel #23
0
 def __init__(self, width, height, dpi):
     if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
     RendererBase.__init__(self)
     self.texd = maxdict(50)  # a cache of tex image rasters
     self._fontd = maxdict(50)
     self.dpi = dpi
     self.width = width
     self.height = height
     if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying')
     self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False)
     self._filter_renderers = []
     if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done',
                                  'debug-annoying')
     self._update_methods()
     self.mathtext_parser = MathTextParser('Agg')
     self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
     if __debug__: verbose.report('RendererAgg.__init__ done',
                                  'debug-annoying')
Beispiel #24
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._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))
Beispiel #25
0
 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
Beispiel #26
0
    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))
Beispiel #27
0
    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")
Beispiel #28
0
 def mathtext_parser(self):
     return MathTextParser('Cairo')
Beispiel #29
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:
            clip.append('%s clipbox' % _nums_to_str(*bbox.size, *bbox.p0))
        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)

        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)
            self._pswriter.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

    @cbook._delete_parameter("3.3", "ismath")
    def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
        # docstring inherited
        if not hasattr(self, "psfrag"):
            _log.warning(
                "The PS backend determines usetex status solely based on "
                "rcParams['text.usetex'] and does not support having "
                "usetex=True only for some elements; this element will thus "
                "be rendered as if usetex=False.")
            self.draw_text(gc, x, y, s, prop, angle, False, mtext)
            return

        w, h, bl = self.get_text_width_height_descent(s, prop, ismath="TeX")
        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 dict.__getitem__(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

        if debugPS:
            self._pswriter.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:
            write('%1.4g %1.4g %1.4g %1.4g clipbox\n'
                  % (*cliprect.size, *cliprect.p0))
        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")
Beispiel #30
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):
        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 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.
        """
        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

    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 = 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] = self.glyph_to_path(font)

            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):
        """
        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 = 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] = 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()

        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 = 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.
        # oldfont, seq = None, []
        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] = 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)

    @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
Beispiel #31
0
                        unicode_literals)

import six

import os
import sys
from hashlib import md5

from docutils import nodes
from docutils.parsers.rst import directives
import warnings

from matplotlib import rcParams
from matplotlib.mathtext import MathTextParser
rcParams['mathtext.fontset'] = 'cm'
mathtext_parser = MathTextParser("Bitmap")


# Define LaTeX math node:
class latex_math(nodes.General, nodes.Element):
    pass


def fontset_choice(arg):
    return directives.choice(arg, ['cm', 'stix', 'stixsans'])


options_spec = {'fontset': fontset_choice}


def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
Beispiel #32
0
class RendererCairo(RendererBase):
    fontweights = _api.deprecated("3.3")(property(lambda self: {*_f_weights}))
    fontangles = _api.deprecated("3.3")(property(lambda self: {*_f_angles}))
    mathtext_parser = _api.deprecated("3.4")(
        property(lambda self: MathTextParser('Cairo')))

    def __init__(self, dpi):
        self.dpi = dpi
        self.gc = GraphicsContextCairo(renderer=self)
        self.text_ctx = cairo.Context(
            cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
        super().__init__()

    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.save()
            ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
            ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72)
            opts = cairo.FontOptions()
            opts.set_antialias(
                cairo.Antialias.DEFAULT if mpl.
                rcParams["text.antialiased"] else cairo.Antialias.NONE)
            ctx.set_font_options(opts)
            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._text2path.mathtext_parser.parse(s, self.dpi, prop)

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

        for font, fontsize, idx, 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(chr(idx))

        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:
            width, height, descent, *_ = \
                self._text2path.mathtext_parser.parse(s, self.dpi, prop)
            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
Beispiel #33
0
class RendererSVG(RendererBase):
    def __init__(self, width, height, svgwriter, basename=None, image_dpi=72,
                 *, metadata=None):
        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_metadata(metadata)
        self._write_default_style()

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

    def _write_metadata(self, metadata):
        # Add metadata following the Dublin Core Metadata Initiative, and the
        # Creative Commons Rights Expression Language. This is mainly for
        # compatibility with Inkscape.
        if metadata is None:
            metadata = {}
        metadata = {
            'Format': 'image/svg+xml',
            'Type': 'http://purl.org/dc/dcmitype/StillImage',
            'Creator':
                f'Matplotlib v{mpl.__version__}, https://matplotlib.org/',
            **metadata
        }
        writer = self.writer

        if 'Title' in metadata:
            writer.element('title', text=metadata['Title'])

        # Special handling.
        date = metadata.get('Date', None)
        if date is not None:
            if isinstance(date, str):
                dates = [date]
            elif isinstance(date, (datetime.datetime, datetime.date)):
                dates = [date.isoformat()]
            elif np.iterable(date):
                dates = []
                for d in date:
                    if isinstance(d, str):
                        dates.append(d)
                    elif isinstance(d, (datetime.datetime, datetime.date)):
                        dates.append(d.isoformat())
                    else:
                        raise ValueError(
                            'Invalid type for Date metadata. '
                            'Expected iterable of str, date, or datetime, '
                            'not {!r}.'.format(type(d)))
            else:
                raise ValueError('Invalid type for Date metadata. '
                                 'Expected str, date, datetime, or iterable '
                                 'of the same, not {!r}.'.format(type(date)))
            metadata['Date'] = '/'.join(dates)
        else:
            # Get source date from SOURCE_DATE_EPOCH, if set.
            # See https://reproducible-builds.org/specs/source-date-epoch/
            date = os.getenv("SOURCE_DATE_EPOCH")
            if date:
                date = datetime.datetime.utcfromtimestamp(int(date))
                metadata['Date'] = date.replace(tzinfo=UTC).isoformat()
            else:
                metadata['Date'] = datetime.datetime.today().isoformat()

        mid = writer.start('metadata')
        writer.start('rdf:RDF', attrib={
                'xmlns:dc': "http://purl.org/dc/elements/1.1/",
                'xmlns:cc': "http://creativecommons.org/ns#",
                'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
            })
        writer.start('cc:Work')

        uri = metadata.pop('Type', None)
        if uri is not None:
            writer.element('dc:type', attrib={'rdf:resource': uri})

        # Single value only.
        for key in ['title', 'coverage', 'date', 'description', 'format',
                    'identifier', 'language', 'relation', 'source']:
            info = metadata.pop(key.title(), None)
            if info is not None:
                writer.element(f'dc:{key}', text=info)

        # Multiple Agent values.
        for key in ['creator', 'contributor', 'publisher', 'rights']:
            agents = metadata.pop(key.title(), None)
            if agents is None:
                continue

            if isinstance(agents, str):
                agents = [agents]

            writer.start(f'dc:{key}')
            for agent in agents:
                writer.start('cc:Agent')
                writer.element('dc:title', text=agent)
                writer.end('cc:Agent')
            writer.end(f'dc:{key}')

        # Multiple values.
        keywords = metadata.pop('Keywords', None)
        if keywords is not None:
            if isinstance(keywords, str):
                keywords = [keywords]

            writer.start('dc:subject')
            writer.start('rdf:Bag')
            for keyword in keywords:
                writer.element('rdf:li', text=keyword)
            writer.end('rdf:Bag')
            writer.end('dc:subject')

        writer.close(mid)

        if metadata:
            raise ValueError('Unknown metadata key(s) passed to SVG writer: ' +
                             ','.join(metadata))

    def _write_default_style(self):
        writer = self.writer
        default_style = generate_css({
            'stroke-linejoin': 'round',
            'stroke-linecap': 'butt'})
        writer.start('defs')
        writer.element('style', type='text/css', text='*{%s}' % default_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())])
            attrib['style'] = (
                'image-rendering:crisp-edges;'
                'image-rendering:pixelated')
            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')

    @cbook._delete_parameter("3.3", "ismath")
    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)
Beispiel #34
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

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

        Returns
        -------
        verts : list
            A list of numpy arrays containing the x and y coordinates of the
            vertices.

        codes : list
            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
Beispiel #35
0
 def mathtext_parser(self):
     return MathTextParser('SVG')
Beispiel #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, 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()))

        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.save()
        ctx.set_source_surface(surface, x, 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 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
 def mathtext_parser(self):
     return MathTextParser("PS")
Beispiel #38
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 _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_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=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'])

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

        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(
                    str(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_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.
        """

        # 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._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)
Beispiel #39
0
class RendererSVG(RendererBase):
    FONT_SCALE = 100.0
    fontd = maxdict(50)

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

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

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

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

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

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

        path_data = "".join(cmd)
        return path_data

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

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

        offset, seq = gc.get_dashes()
        if seq is None:
            dashes = ''
        else:
            dashes = 'stroke-dasharray: %s; stroke-dashoffset: %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()
        clippath = gc.get_clip_path()
        if cliprect is None and clippath is None:
            return '', None
        elif clippath is not None:
            # See if we've already seen this clip rectangle
            key = hash(clippath)
            if self._clipd.get(key) is None:  # If not, store a new clipPath
                self._clipd[key] = clippath
                style = "stroke: gray; fill: none;"
                path_data = self._path_commands(clippath)
                path = """\
<defs>
    <clipPath id="%(key)s">
    <path d="%(path_data)s"/>
    </clipPath>
</defs>
""" % locals()
                return path, key
            else:
                return '', key
        elif cliprect is not None:
            # See if we've already seen this clip rectangle
            key = hash(cliprect)
            if self._clipd.get(key) is None:  # If not, store a new clipPath
                self._clipd[key] = cliprect
                x, y, w, h = cliprect
                y = self.height - (y + h)
                style = "stroke: gray; fill: none;"
                box = """\
<defs>
    <clipPath id="%(key)s">
    <rect x="%(x)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_path(self, gc, rgbFace, path):
        path_data = self._path_commands(path)
        self._draw_svg_element("path", 'd="%s"' % path_data, gc, rgbFace)

    def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2,
                 rotation):
        """
        Ignores angles for now
        """
        details = 'cx="%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())
        write = self._svgwriter.write

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

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

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

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

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

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

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

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

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

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

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

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

        style = "fill: %s" % color

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

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

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

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

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

            curr_x, curr_y = 0.0, 0.0

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

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

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

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

                thetext = escape_xml_text(thetext)

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

                curr_x = new_x + xadvance
                curr_y = new_y

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

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

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

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

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

    def flipy(self):
        return True

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

    def get_text_width_height_descent(self, s, prop, ismath):
        if ismath:
            width, height, descent, trash, used_characters = \
                self.mathtext_parser.parse(s, 72, prop)
            return width, height, descent
        font = self._get_font(prop)
        font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
        w, h = font.get_width_height()
        w /= 64.0  # convert from subpixels
        h /= 64.0
        d = font.get_descent()
        d /= 64.0
        return w, h, d
Beispiel #40
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 _do_clip(self, ctx, cliprect, clippath):
        if cliprect is not None:
            x, y, w, h = cliprect.bounds
            # pixel-aligned clip-regions are faster
            x, y, w, h = round(x), round(y), round(w), round(h)
            ctx.new_path()
            ctx.rectangle(x, self.height - h - y, w, h)
            ctx.clip()

        if clippath is not None:
            tpath, affine = clippath.get_transformed_path_and_affine()
            ctx.new_path()
            affine = affine + Affine2D().scale(1.0, -1.0).translate(
                0.0, self.height)
            tpath = affine.transform_path(tpath)
            RendererCairo.convert_path(ctx, tpath)
            ctx.clip()

    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
        ctx.save()
        self._do_clip(ctx, gc._cliprect, gc._clippath)

        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())
        ctx.restore()

    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
        ctx.save()
        if clippath is not None:
            tpath = clippath_trans.transform_path(clippath)
            ctx.new_path()
            RendererCairo.convert_path(ctx, tpath)
            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):
        # 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
Beispiel #41
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:
            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)
            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
Beispiel #42
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
Beispiel #43
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.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):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_transform = gc.get_clip_path()
        if all_transforms:
            transforms = [
                numpy.dot(master_transform, t) for t in all_transforms
            ]
        else:
            transforms = [master_transform]
        gc.draw_path_collection(cliprect, clippath, clippath_transform, paths,
                                transforms, offsets, offsetTrans, facecolors,
                                edgecolors, linewidths, linestyles,
                                antialiaseds)

    def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
                       coordinates, offsets, offsetTrans, facecolors,
                       antialiased, showedges):
        cliprect = gc.get_clip_rectangle()
        clippath, clippath_transform = gc.get_clip_path()
        gc.draw_quad_mesh(master_transform, cliprect, clippath,
                          clippath_transform, meshWidth, meshHeight,
                          coordinates, offsets, offsetTrans, facecolors,
                          antialiased, showedges)

    def new_gc(self):
        self.gc.save()
        self.gc.set_hatch(None)
        return self.gc

    def draw_image(self, gc, x, y, im):
        im.flipud_out()
        nrows, ncols, data = im.as_rgba_str()
        gc.draw_image(x, y, nrows, ncols, data, gc.get_clip_rectangle(),
                      *gc.get_clip_path())
        im.flipud_out()

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

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

    def _draw_mathtext(self, gc, x, y, s, prop, angle):
        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
Beispiel #44
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, dpi=72):
        RendererBase.__init__(self)
        self.width = width
        self.height = height
        self._pswriter = pswriter
        if rcParams['text.usetex']:
            self.textcnt = 0
            self.psfrag = []

        # 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 = dpi / 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(s)

    def merge_used_characters(self, other):
        for stat_key, (realpath, set) in other.items():
            used_characters = self.used_characters.setdefault(
                stat_key, (realpath, Set()))
            used_characters[1].update(set)

    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(0, hatches['horiz']))
        self._pswriter.write(do_hatch(90, 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):
        path = transform.transform_path(path)

        ps = []
        for points, code in path.iter_segments():
            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:
                ps.append("%g %g %g %g %g %g c" %
                          (points[0], points[1], points[0], points[1],
                           points[2], points[3]))
            elif code == Path.CURVE4:
                ps.append("%g %g %g %g %g %g c" % tuple(points))
            elif code == Path.CLOSEPOLY:
                ps.append("cl")
        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._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 x, y in tpath.vertices:
            ps_cmd.append("%1.3g %1.3g 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):
        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):

            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()
        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()
            #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, thisy = 0, 0
            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)

            self.set_color(*gc.get_rgb())
            self.set_font(font.get_sfnt()[(1, 0, 0, 6)],
                          prop.get_size_in_points())
            self.track_characters(font, s)

            cmap = font.get_charmap()
            lastgind = None
            #print 'text', s
            lines = []
            thisx, thisy = 0, 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.
        """
        # local variable eliminates all repeated attribute lookups
        write = self._pswriter.write
        if debugPS and command:
            write("% " + command + "\n")

        stroke = (stroke and gc.get_linewidth() > 0.0
                  and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0))
        fill = (fill and rgbFace is not None
                and (len(rgbFace) <= 3 or rgbFace[3] != 0.0))

        if stroke:
            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())
            if self.linewidth > 0 and stroke:
                self.set_color(*gc.get_rgb()[:3])

        cliprect = gc.get_clip_rectangle()
        if cliprect:
            x, y, w, h = cliprect.bounds
            write('gsave\n%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('gsave\n%s\n' % id)

        # Jochen, is the strip necessary? - this could be a honking big string
        write(ps.strip())
        write("\n")

        if fill:
            #print 'rgbface', rgbFace
            write("gsave\n")
            self.set_color(store=0, *rgbFace[:3])
            write("fill\ngrestore\n")

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

        if self.linewidth > 0 and stroke:
            self.set_color(*gc.get_rgb()[:3])
            write("stroke\n")
        else:
            write("newpath\n")

        if clippath:
            write("grestore\n")
        if cliprect:
            write("grestore\n")
class RendererMac(RendererBase):
    """
    The renderer handles drawing/rendering operations. Most of the renderer's
    methods forward the command to the renderer's graphics context. The
    renderer does not wrap a C object and is written in pure Python.
    """

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

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

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

    def draw_path(self, gc, path, transform, rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        linewidth = gc.get_linewidth()
        gc.draw_path(path, transform, linewidth, rgbFace)

    def draw_markers(self,
                     gc,
                     marker_path,
                     marker_trans,
                     path,
                     trans,
                     rgbFace=None):
        if rgbFace is not None:
            rgbFace = tuple(rgbFace)
        linewidth = gc.get_linewidth()
        gc.draw_markers(marker_path, marker_trans, path, trans, linewidth,
                        rgbFace)

    def draw_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):
        if offset_position == 'data':
            offset_position = True
        else:
            offset_position = False
        path_ids = []
        for path, transform in self._iter_collection_raw_paths(
                master_transform, paths, all_transforms):
            path_ids.append((path, transform))
        master_transform = master_transform.get_matrix()
        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 get_image_magnification(self):
        return self.gc.get_image_magnification()

    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
        scale = self.gc.get_image_magnification()
        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 * scale)
            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):
        scale = self.gc.get_image_magnification()
        ox, oy, width, height, descent, image, used_characters = \
            self.mathtext_parser.parse(s, self.dpi*scale, 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, six.text_type(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(
            six.text_type(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
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"

    mathtext_parser = _api.deprecated("3.4")(
        property(lambda self: MathTextParser("PS")))

    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._logwarn_once = functools.lru_cache(None)(_log.warning)

    def _is_transparent(self, rgb_or_rgba):
        if rgb_or_rgba is None:
            return True  # Consistent with rgbFace semantics.
        elif len(rgb_or_rgba) == 4:
            if rgb_or_rgba[3] == 0:
                return True
            if rgb_or_rgba[3] != 1:
                self._logwarn_once(
                    "The PostScript backend does not support transparency; "
                    "partially transparent artists will be rendered opaque.")
            return False
        else:  # len() == 3.
            return False

    def set_color(self, r, g, b, store=True):
        if (r, g, b) != self.color:
            self._pswriter.write(f"{r:1.3f} setgray\n" if r == g == b else
                                 f"{r:1.3f} {g:1.3f} {b:1.3f} setrgbcolor\n")
            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

    @staticmethod
    def _linejoin_cmd(linejoin):
        # Support for directly passing integer values is for backcompat.
        linejoin = {
            'miter': 0,
            'round': 1,
            'bevel': 2,
            0: 0,
            1: 1,
            2: 2
        }[linejoin]
        return f"{linejoin:d} setlinejoin\n"

    def set_linejoin(self, linejoin, store=True):
        if linejoin != self.linejoin:
            self._pswriter.write(self._linejoin_cmd(linejoin))
            if store:
                self.linejoin = linejoin

    @staticmethod
    def _linecap_cmd(linecap):
        # Support for directly passing integer values is for backcompat.
        linecap = {
            'butt': 0,
            'round': 1,
            'projecting': 2,
            0: 0,
            1: 1,
            2: 2
        }[linecap]
        return f"{linecap:d} setlinecap\n"

    def set_linecap(self, linecap, store=True):
        if linecap != self.linecap:
            self._pswriter.write(self._linecap_cmd(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

        self._pswriter.write(f"[{_nums_to_str(*seq)}]"
                             f" {_nums_to_str(offset)} setdash\n" if seq
                             is not None and len(seq) else "[] 0 setdash\n")
        if store:
            self.linedash = (offset, seq)

    def set_font(self, fontname, fontsize, store=True):
        if (fontname, fontsize) != (self.fontname, self.fontsize):
            self._pswriter.write(f"/{fontname} {fontsize:1.3f} selectfont\n")
            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 _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_cmd(self, gc):
        clip = []
        rect = gc.get_clip_rectangle()
        if rect is not None:
            clip.append("%s clipbox\n" % _nums_to_str(*rect.size, *rect.p0))
        path, trf = gc.get_clip_path()
        if path is not None:
            key = (path, id(trf))
            custom_clip_cmd = self._clip_paths.get(key)
            if custom_clip_cmd is None:
                custom_clip_cmd = "c%d" % len(self._clip_paths)
                self._pswriter.write(f"""\
/{custom_clip_cmd} {{
{self._convert_path(path, trf, simplify=False)}
clip
newpath
}} bind def
""")
                self._clip_paths[key] = custom_clip_cmd
            clip.append(f"{custom_clip_cmd}\n")
        return "".join(clip)

    @_log_if_debug_on
    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

        self._pswriter.write(f"""\
gsave
{self._get_clip_cmd(gc)}
{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
""")

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

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

        ps_color = (None
                    if self._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)
            ps_cmd.append(self._linejoin_cmd(gc.get_joinstyle()))
            ps_cmd.append(self._linecap_cmd(gc.get_capstyle()))

        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)

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

        path_codes = []
        for i, (path, transform) in enumerate(
                self._iter_collection_raw_paths(master_transform, paths,
                                                all_transforms)):
            name = 'p%d_%d' % (self._path_collection_id, i)
            path_bytes = self._convert_path(path, transform, simplify=False)
            self._pswriter.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

    @_log_if_debug_on
    def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
        # docstring inherited
        if self._is_transparent(gc.get_rgb()):
            return  # Special handling for fully transparent.

        if not hasattr(self, "psfrag"):
            self._logwarn_once(
                "The PS backend determines usetex status solely based on "
                "rcParams['text.usetex'] and does not support having "
                "usetex=True only for some elements; this element will thus "
                "be rendered as if usetex=False.")
            self.draw_text(gc, x, y, s, prop, angle, False, mtext)
            return

        w, h, bl = self.get_text_width_height_descent(s, prop, ismath="TeX")
        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)

        # Stick to the bottom alignment.
        pos = _nums_to_str(x, 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

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

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

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

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

        if mpl.rcParams['ps.useafm']:
            font = self._get_font_afm(prop)
            scale = 0.001 * prop.get_size_in_points()

            thisx = 0
            last_name = None  # kerns returns 0 for None.
            xs_names = []
            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('?')
                kern = font.get_kern_dist_from_name(last_name, name)
                last_name = name
                thisx += kern * scale
                xs_names.append((thisx, name))
                thisx += width * scale

        else:
            font = self._get_font_ttf(prop)
            font.set_text(s, 0, flags=LOAD_NO_HINTING)
            self._character_tracker.track(font, s)
            xs_names = [(item.x, font.get_glyph_name(item.glyph_idx))
                        for item in _text_helpers.layout(s, font)]

        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"{x:f} 0 m /{name:s} glyphshow"
                            for x, name in xs_names)
        self._pswriter.write(f"""\
gsave
{self._get_clip_cmd(gc)}
{x:f} {y:f} translate
{angle:f} rotate
{thetext}
grestore
""")

    @_log_if_debug_on
    def draw_mathtext(self, gc, x, y, s, prop, angle):
        """Draw the math text using matplotlib.mathtext."""
        width, height, descent, glyphs, rects = \
            self._text2path.mathtext_parser.parse(
                s, 72, prop,
                _force_standard_ps_fonts=mpl.rcParams["ps.useafm"])
        self.set_color(*gc.get_rgb())
        self._pswriter.write(f"gsave\n"
                             f"{x:f} {y:f} translate\n"
                             f"{angle:f} rotate\n")
        lastfont = None
        for font, fontsize, num, ox, oy in glyphs:
            self._character_tracker.track_glyph(font, num)
            if (font.postscript_name, fontsize) != lastfont:
                lastfont = font.postscript_name, fontsize
                self._pswriter.write(
                    f"/{font.postscript_name} {fontsize} selectfont\n")
            symbol_name = (font.get_name_char(chr(num)) if isinstance(
                font, AFM) else font.get_glyph_name(font.get_char_index(num)))
            self._pswriter.write(f"{ox:f} {oy:f} moveto\n"
                                 f"/{symbol_name} glyphshow\n")
        for ox, oy, w, h in rects:
            self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n")
        self._pswriter.write("grestore\n")

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

    @_log_if_debug_on
    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):
        """
        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.
        """
        write = self._pswriter.write
        mightstroke = (gc.get_linewidth() > 0
                       and not self._is_transparent(gc.get_rgb()))
        if not mightstroke:
            stroke = False
        if self._is_transparent(rgbFace):
            fill = False
        hatch = gc.get_hatch()

        if mightstroke:
            self.set_linewidth(gc.get_linewidth())
            self.set_linejoin(gc.get_joinstyle())
            self.set_linecap(gc.get_capstyle())
            self.set_linedash(*gc.get_dashes())
            self.set_color(*gc.get_rgb()[:3])
        write('gsave\n')

        write(self._get_clip_cmd(gc))

        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")
Beispiel #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._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))
            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)
            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
                                             and gc.get_hatch_path() 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, 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)
            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_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)
                    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)  #_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)
                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 ismath == "TeX":
            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
Beispiel #48
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._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))
            self._svgwriter.write(
                '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' %
                (HATCH_SIZE + 1, HATCH_SIZE + 1, rgb2hex(rgbFace)))
            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, 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
Beispiel #49
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])
Beispiel #50
0
 def __init__(self):
     self.mathtext_parser = MathTextParser('path')
     self._texmanager = None
Beispiel #51
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 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 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)
Beispiel #52
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()

    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
Beispiel #53
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):
        # 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_path_collection(self, gc, master_transform, paths, all_transforms,
                             offsets, offsetTrans, facecolors, edgecolors,
                             linewidths, linestyles, antialiaseds, urls,
                             offset_position):

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

        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._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(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):
        # docstring inherited
        return self.width, self.height

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

        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):
        # 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
Beispiel #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 = 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.
        """
        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.

        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)
Beispiel #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()
    _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)
Beispiel #56
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  # 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()
        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._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 = hashlib.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].decode("mac_roman"))
            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)
        style = generate_css(
            {k: v
             for k, v in six.iteritems(style) 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):
        # 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])
            _log.info('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.

        Parameters
        ----------
        prop : `matplotlib.font_manager.FontProperties`
          font property

        s : str
          text to be converted

        usetex : bool
          If True, use matplotlib usetex mode.

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

            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 range(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)
Beispiel #57
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
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=False, mtext=None):
        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
Beispiel #59
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
Beispiel #60
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()