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
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
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 __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")
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 __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')
def __init__(self, width, height, svgwriter, basename=None, image_dpi=72): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self.image_dpi = image_dpi # the actual dpi we want to rasterize stuff with self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = OrderedDict() self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = OrderedDict() self._has_gouraud = False self._n_gradients = 0 self._fonts = OrderedDict() self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', width='%ipt' % width, height='%ipt' % height, viewBox='0 0 %i %i' % (width, height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_default_style()
def __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)
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 __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 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 __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()
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
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 = []
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 __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 __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 __init__(self, dpi): self.dpi = dpi self.width = 640.0 * dpi / 80 self.height = 480.0 * dpi / 80 mwidth, mheight, width, height = gr.inqdspsize() if (width / (mwidth / 0.0256) < 200): mwidth *= self.width / width gr.setwsviewport(0, mwidth, 0, mwidth * 0.75) else: gr.setwsviewport(0, 0.192, 0, 0.144) gr.setwswindow(0, 1, 0, 0.75) gr.setviewport(0, 1, 0, 0.75) gr.setwindow(0, self.width, 0, self.height) self.mathtext_parser = MathTextParser('agg') self.texmanager = TexManager()
def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter self._groupd = {} if not rcParams["svg.image_inline"]: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self.mathtext_parser = MathTextParser("SVG") svgwriter.write(svgProlog % (width, height, width, height))
def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying')
def __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 __init__(self, width, height, ctx, dpi=72): self.width = width self.height = height self.dpi = dpi self.ctx = ctx self._image_count = 0 # used to uniquely label each image created in this figure... # define the js context self.ctx.width = width self.ctx.height = height #self.ctx.textAlign = "center"; self.ctx.textBaseline = "alphabetic" self.flip = Affine2D().scale(1, -1).translate(0, height) self.mathtext_parser = MathTextParser('bitmap') self._path_time = 0 self._text_time = 0 self._marker_time = 0 self._sub_time = 0 self._last_clip = None self._last_clip_path = None self._clip_count = 0
def __init__(self, width, height, svgwriter, basename=None): self.width=width self.height=height self._svgwriter = svgwriter if rcParams['path.simplify']: self.simplify = (width, height) else: self.simplify = None self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self.mathtext_parser = MathTextParser('SVG') svgwriter.write(svgProlog%(width,height,width,height))
def __init__(self, width, height, dpi): if __debug__: verbose.report("RendererAgg.__init__", "debug-annoying") RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report("RendererAgg.__init__ width=%s, height=%s" % (width, height), "debug-annoying") self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report("RendererAgg.__init__ _RendererAgg done", "debug-annoying") self._update_methods() self.mathtext_parser = MathTextParser("Agg") self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report("RendererAgg.__init__ done", "debug-annoying")
def mathtext_parser(self): return MathTextParser('Cairo')
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")
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
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=[]):
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
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)
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
def mathtext_parser(self): return MathTextParser('SVG')
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")
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)
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
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
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
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
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
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")
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
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
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])
def __init__(self): self.mathtext_parser = MathTextParser('path') self._texmanager = None
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)
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
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
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)
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)
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)
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
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
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()