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
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()
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter if rcParams["path.simplify"]: self.simplify = (width, height) else: self.simplify = None self._groupd = {} if not rcParams["svg.image_inline"]: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self.mathtext_parser = MathTextParser("SVG") svgwriter.write(svgProlog % (width, height, width, height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = "" else: clippath = 'clip-path="url(#%s)"' % clipid if gc.get_url() is not None: self._svgwriter.write('<a xlink:href="%s">' % gc.get_url()) style = self._get_style(gc, rgbFace) self._svgwriter.write('<%s style="%s" %s %s/>\n' % (element, style, clippath, details)) if gc.get_url() is not None: self._svgwriter.write("</a>") def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if rgbFace is None: fill = "none" else: fill = rgb2hex(rgbFace[:3]) offset, seq = gc.get_dashes() if seq is None: dashes = "" else: dashes = "stroke-dasharray: %s; stroke-dashoffset: %f;" % (",".join(["%f" % val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return ( "fill: %s; stroke: %s; stroke-width: %f; " "stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f" % ( fill, rgb2hex(gc.get_rgb()[:3]), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) ) else: return "fill: %s; opacity: %f" % (fill, gc.get_alpha()) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: path_data = self._convert_path(clippath, clippath_trans) path = '<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) path = '<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals() else: return None id = self._clipd.get(path) if id is None: id = "p%s" % md5(path).hexdigest() self._svgwriter.write('<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write("\n </clipPath>\n</defs>") self._clipd[path] = id return id def open_group(self, s): self._groupd[s] = self._groupd.get(s, 0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write("</g>\n") def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams["svg.image_noscale"] _path_commands = { Path.MOVETO: "M%f %f", Path.LINETO: "L%f %f", Path.CURVE3: "Q%f %f %f %f", Path.CURVE4: "C%f %f %f %f %f %f", } def _make_flip_transform(self, transform): return transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height) def _convert_path(self, path, transform, simplify=None): tpath = transform.transform_path(path) path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 for points, code in tpath.iter_segments(simplify): if code == Path.CLOSEPOLY: segment = "z" else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender("\n") currpos = 0 appender(segment) currpos += len(segment) return "".join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) path_data = self._convert_path(path, trans_and_flip, self.simplify) self._draw_svg_element("path", 'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) name = self._markers.get(key) if name is None: name = "m%s" % md5(key).hexdigest() write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = "" else: clippath = 'clip-path="url(#%s)"' % clipid write("<g %s>" % clippath) trans_and_flip = self._make_flip_transform(trans) tpath = trans_and_flip.transform_path(path) for vertices, code in tpath.iter_segments(): if len(vertices): x, y = vertices[-2:] details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) write("</g>") def draw_path_collection( self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, ): write = self._svgwriter.write path_codes = [] write("<defs>\n") for i, (path, transform) in enumerate(self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform) name = "coll%x_%x_%s" % (self._path_collection_id, i, md5(d).hexdigest()) write('<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write("</defs>\n") for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, ): clipid = self._get_gc_clip_svg(gc) url = gc.get_url() if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) if clipid is not None: write('<g clip-path="url(#%s)">' % clipid) details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write("</g>") if url is not None: self._svgwriter.write("</a>") self._path_collection_id += 1 def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): # MGDTODO: Support clippath here trans = [1, 0, 0, 1, 0, 0] transstr = "" if rcParams["svg.image_noscale"]: trans = list(im.get_matrix()) trans[5] = -trans[5] transstr = 'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() url = getattr(im, "_url", None) if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) self._svgwriter.write( '<image x="%f" y="%f" width="%f" height="%f" ' '%s xlink:href="' % (x / trans[0], (self.height - y) / trans[3] - h, w, h, transstr) ) if rcParams["svg.image_inline"]: self._svgwriter.write("data:image/png;base64,\n") stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() self._svgwriter.write(base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = "%s.image%d.png" % (self.basename, self._imaged[self.basename]) verbose.report("Writing image file for inclusion: %s" % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write('"/>\n') if url is not None: self._svgwriter.write("</a>") def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write if rcParams["svg.embed_char_paths"]: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write("<defs>\n") for path in new_chars: write(path) write("</defs>\n") svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append("translate(%f,%f)rotate(%1.1f)" % (x, y, -angle)) elif x != 0 or y != 0: svg.append("translate(%f,%f)" % (x, y)) svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord("?") gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' x="%f"' % (currx * (self.FONT_SCALE / fontsize))) svg.append("/>\n") currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append("</g>\n") if clipid is not None: svg.append("</g>\n") svg = "".join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = "font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f" % ( fontsize, fontfamily, fontstyle, color, gc.get_alpha(), ) if angle != 0: transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y) # Inkscape doesn't support rotate(angle x y) else: transform = "" svg = ( """\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() ) write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote("%s-%d" % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append( "q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry) ) elif step[0] == 3: # CURVE4 path_data.append( "c%f %f %f %f %f %f" % ( step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry, ) ) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = "".join(path_data) char_num = "c_%s" % md5(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, "".join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote("%s-%d" % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write style = "fill: %s" % color if rcParams["svg.embed_char_paths"]: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write("<defs>\n") for path in new_chars: write(path) write("</defs>\n") svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append("translate(%f,%f)rotate(%1.1f)" % (x, y, -angle)) else: svg.append("translate(%f,%f)" % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append( '<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE) ) svg.append("</g>\n") else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append( ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y) ) # Inkscape doesn't support rotate(angle x y) svg.append(">\n") curr_x, curr_y = 0.0, 0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = -new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append(">%s</tspan>\n" % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append("</text>\n") if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append("translate(%f,%f) rotate(%1.1f)" % (x, y, -angle)) else: svg.append("translate(%f,%f)" % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append( '<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height) ) svg.append("</g>") self.open_group("mathtext") write("".join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write("</svg>\n") def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, trash, used_characters = self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'regular' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): self.dpi = dpi self.gc = GraphicsContextCairo(renderer=self) self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) # Although it may appear natural to automatically call # `self.set_width_height(surface.get_width(), surface.get_height())` # here (instead of having the caller do so separately), this would fail # for PDF/PS/SVG surfaces, which have no way to report their extents. def set_width_height(self, width, height): self.width = width self.height = height def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: ctx.save() if len(fill_c) == 3 or alpha_overrides: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod @cbook.deprecated("3.0") def convert_path(ctx, path, transform, clip=None): _append_path(ctx, path, transform, clip) def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx # Clip the path to the actual rendering extents if it isn't filled. clip = (ctx.clip_extents() if rgbFace is None and gc.get_hatch() is None else None) transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() _append_path(ctx, path, transform, clip) self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None): ctx = gc.ctx ctx.new_path() # Create the path for the marker; it needs to be flipped here already! _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill x1, y1, x2, y2 = ctx.fill_extents() if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0: filled = False # No fill, just unset this (so we don't try to fill it later on) rgbFace = None else: filled = True transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() for i, (vertices, codes) in enumerate( path.iter_segments(transform, simplify=False)): if len(vertices): x, y = vertices[-2:] ctx.save() # Translate and apply path ctx.translate(x, y) ctx.append_path(marker_path) ctx.restore() # Slower code path if there is a fill; we need to draw # the fill and stroke for each marker at the same time. # Also flush out the drawing every once in a while to # prevent the paths from getting way too long. if filled or i % 1000 == 0: self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) # Fast path, if there is no fill, draw everything in one step if not filled: self._fill_and_stroke( ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path_ids = [] for path, transform in self._iter_collection_raw_paths( master_transform, paths, all_transforms): path_ids.append((path, Affine2D(transform))) reuse_key = None grouped_draw = [] def _draw_paths(): if not grouped_draw: return gc_vars, rgb_fc = reuse_key gc = copy.copy(gc0) # We actually need to call the setters to reset the internal state. vars(gc).update(gc_vars) for k, v in gc_vars.items(): if k == "_linestyle": # Deprecated, no effect. continue try: getattr(gc, "set" + k)(v) except (AttributeError, TypeError) as e: pass gc.ctx.new_path() paths, transforms = zip(*grouped_draw) grouped_draw.clear() _append_paths(gc.ctx, paths, transforms) self._fill_and_stroke( gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha()) for xo, yo, path_id, gc0, rgb_fc in self._iter_collection( gc, master_transform, all_transforms, path_ids, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id transform = (Affine2D(transform.get_matrix()) .translate(xo, yo - self.height).scale(1, -1)) # rgb_fc could be a ndarray, for which equality is elementwise. new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None if new_key == reuse_key: grouped_draw.append((path, transform)) else: _draw_paths() grouped_draw.append((path, transform)) reuse_key = new_key _draw_paths() def draw_image(self, gc, x, y, im): im = cbook._unmultipled_rgba8888_to_premultiplied_argb32(im[::-1]) surface = cairo.ImageSurface.create_for_data( im.ravel().data, cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1] * 4) ctx = gc.ctx y = self.height - y - im.shape[0] ctx.save() ctx.set_source_surface(surface, float(x), float(y)) ctx.paint() ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to(x, y) ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate(np.deg2rad(-angle)) ctx.set_font_size(size) ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate(np.deg2rad(-angle)) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.select_font_face(fontProp.name, self.fontangles[fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) ctx.show_text(s) for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle(ox, oy, w, h) ctx.set_source_rgb(0, 0, 0) ctx.fill_preserve() ctx.restore() def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size(size) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): self.gc.ctx.save() self.gc._alpha = 1 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): return points / 72 * self.dpi
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None, image_dpi=72): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self.image_dpi = image_dpi # the actual dpi we want to rasterize stuff with self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = OrderedDict() self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = OrderedDict() self._has_gouraud = False self._n_gradients = 0 self._fonts = OrderedDict() self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', width='%ipt' % width, height='%ipt' % height, viewBox='0 0 %i %i' % (width, height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) self.writer.flush() def _write_default_style(self): writer = self.writer default_style = generate_css({ 'stroke-linejoin': 'round', 'stroke-linecap': 'butt'}) writer.start('defs') writer.start('style', type='text/css') writer.data('*{%s}\n' % default_style) writer.end('style') writer.end('defs') def _make_id(self, type, content): content = str(content) if rcParams['svg.hashsalt'] is None: salt = str(uuid.uuid4()) else: salt = rcParams['svg.hashsalt'] if six.PY3: content = content.encode('utf8') salt = salt.encode('utf8') m = md5() m.update(salt) m.update(content) return '%s%s' % (type, m.hexdigest()[:10]) def _make_flip_transform(self, transform): return (transform + Affine2D() .scale(1.0, -1.0) .translate(0.0, self.height)) def _get_font(self, prop): fname = findfont(prop) font = get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ if rgbFace is not None: rgbFace = tuple(rgbFace) edge = gc.get_rgb() if edge is not None: edge = tuple(edge) dictkey = (gc.get_hatch(), rgbFace, edge) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start('defs') for ((path, face, stroke), oid) in six.itervalues(self._hatchd): writer.start( 'pattern', id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=six.text_type(HATCH_SIZE), height=six.text_type(HATCH_SIZE)) path_data = self._convert_path( path, Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if face is None: fill = 'none' else: fill = rgb2hex(face) writer.element( 'rect', x="0", y="0", width=six.text_type(HATCH_SIZE+1), height=six.text_type(HATCH_SIZE+1), fill=fill) writer.element( 'path', d=path_data, style=generate_css({ 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), 'stroke-width': six.text_type(rcParams['hatch.linewidth']), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' }) ) writer.end('pattern') writer.end('defs') def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace """ attrib = {} forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace) if rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) else: if rgbFace is None: attrib['fill'] = 'none' else: if tuple(rgbFace[:3]) != (0, 0, 0): attrib['fill'] = rgb2hex(rgbFace) if len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) if forced_alpha and gc.get_alpha() != 1.0: attrib['opacity'] = short_float_fmt(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib['stroke-dasharray'] = ','.join([short_float_fmt(val) for val in seq]) attrib['stroke-dashoffset'] = short_float_fmt(float(offset)) linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() attrib['stroke'] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: attrib['stroke-opacity'] = short_float_fmt(rgb[3]) if linewidth != 1.0: attrib['stroke-width'] = short_float_fmt(linewidth) if gc.get_joinstyle() != 'round': attrib['stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'butt': attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id('p', dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start('defs') for clip, oid in six.itervalues(self._clipd): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element('path', d=path_data) else: x, y, w, h = clip writer.element( 'rect', x=short_float_fmt(x), y=short_float_fmt(y), width=short_float_fmt(w), height=short_float_fmt(h)) writer.end('clipPath') writer.end('defs') def _write_svgfonts(self): if not rcParams['svg.fonttype'] == 'svgfont': return writer = self.writer writer.start('defs') for font_fname, chars in six.iteritems(self._fonts): font = get_font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() writer.start('font', id=sfnt[(1, 0, 0, 4)]) writer.element( 'font-face', attrib={ 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'units-per-em': '72', 'bbox': ' '.join( short_float_fmt(x / 64.0) for x in font.bbox)}) for char in chars: glyph = font.load_char(char, flags=LOAD_NO_HINTING) verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) # name = font.get_glyph_name(char) writer.element( 'glyph', d=path_data, attrib={ # 'glyph-name': name, 'unicode': unichr(char), 'horiz-adv-x': short_float_fmt(glyph.linearHoriAdvance / 65536.0)}) writer.end('font') writer.end('defs') def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start('g', id="%s_%d" % (s, self._groupd[s])) def close_group(self, s): self.writer.end('g') def option_image_nocomposite(self): """ return whether to generate a composite image from multiple images on a set of axes """ return not rcParams['image.composite_image'] def _convert_path(self, path, transform=None, clip=None, simplify=None, sketch=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_string( path, transform, clip, simplify, sketch, 6, [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path( path, trans_and_flip, clip=clip, simplify=simplify, sketch=gc.get_sketch_params()) attrib = {} attrib['style'] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) self.writer.element('path', d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end('a') def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if not len(path.vertices): return writer = self.writer path_data = self._convert_path( marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) for key in list(six.iterkeys(style)): if not key.startswith('stroke'): del style[key] style = generate_css(style) if oid is None: oid = self._make_id('m', dictkey) writer.start('defs') writer.element('path', id=oid, d=path_data, style=style) writer.end('defs') self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid writer.start('g', attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': '#%s' % oid} clip = (0, 0, self.width*72, self.height*72) for vertices, code in path.iter_segments( trans_and_flip, clip=clip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib['x'] = short_float_fmt(x) attrib['y'] = short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 5) * uses_per_path # cost of definition+use is # (len_path + 3) + 9 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) should_do_optimization = \ len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) writer = self.writer path_codes = [] writer.start('defs') for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = 'C%x_%x_%s' % (self._path_collection_id, i, self._make_id('', d)) writer.element('path', id=oid, d=d) path_codes.append(oid) writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) if clipid is not None: writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) attrib = { 'xlink:href': '#%s' % path_id, 'x': short_float_fmt(xo), 'y': short_float_fmt(self.height - yo), 'style': self._get_style(gc0, rgbFace) } writer.element('use', attrib=attrib) if clipid is not None: writer.end('g') if url is not None: writer.end('a') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start( 'filter', id='colorAdd') writer.element( 'feComposite', attrib={'in': 'SourceGraphic'}, in2='BackgroundImage', operator='arithmetic', k2="1", k3="1") writer.end('filter') avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start('defs') for i in range(3): x1, y1 = tpoints[i] x2, y2 = tpoints[(i + 1) % 3] x3, y3 = tpoints[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start( 'linearGradient', id="GR%x_%d" % (self._n_gradients, i), x1=short_float_fmt(x1), y1=short_float_fmt(y1), x2=short_float_fmt(xb), y2=short_float_fmt(yb)) writer.element( 'stop', offset='0', style=generate_css({'stop-color': rgb2hex(c), 'stop-opacity': short_float_fmt(c[-1])})) writer.element( 'stop', offset='1', style=generate_css({'stop-color': rgb2hex(c), 'stop-opacity': "0"})) writer.end('linearGradient') writer.element( 'polygon', id='GT%x' % self._n_gradients, points=" ".join([short_float_fmt(x) for x in (x1, y1, x2, y2, x3, y3)])) writer.end('defs') avg_color = np.sum(colors[:, :], axis=0) / 3.0 href = '#GT%x' % self._n_gradients writer.element( 'use', attrib={'xlink:href': href, 'fill': rgb2hex(avg_color), 'fill-opacity': short_float_fmt(avg_color[-1])}) for i in range(3): writer.element( 'use', attrib={'xlink:href': href, 'fill': 'url(#GR%x_%d)' % (self._n_gradients, i), 'fill-opacity': '1', 'filter': 'url(#colorAdd)'}) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid self.writer.start('g', attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end('g') def option_scale_image(self): return True def get_image_magnification(self): return self.image_dpi / 72.0 def draw_image(self, gc, x, y, im, transform=None): h, w = im.shape[:2] if w == 0 or h == 0: return attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if rcParams['svg.image_inline']: bytesio = io.BytesIO() _png.write_png(im, bytesio) oid = oid or self._make_id('image', bytesio.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode('ascii')) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) _png.write_png(im, filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename attrib['id'] = oid if transform is None: w = 72.0 * w / self.image_dpi h = 72.0 * h / self.image_dpi self.writer.element( 'image', transform=generate_transform([ ('scale', (1, -1)), ('translate', (0, -h))]), x=short_float_fmt(x), y=short_float_fmt(-(self.height - y - h)), width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = short_float_fmt(alpha) flipped = ( Affine2D().scale(1.0 / w, 1.0 / h) + transform + Affine2D() .translate(x, y) .scale(1.0, -1.0) .translate(0.0, self.height)) attrib['transform'] = generate_transform( [('matrix', flipped.frozen())]) self.writer.element( 'image', width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map=self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = short_float_fmt(gc.get_alpha()) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs if glyph_map_new: writer.start('defs') for char_id, glyph_path in six.iteritems(glyph_map_new): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} attrib['style'] = generate_css(style) font_scale = fontsize / text2path.FONT_SCALE attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,)), ('scale', (font_scale, -font_scale))]) writer.start('g', attrib=attrib) for glyph_id, xposition, yposition, scale in glyph_info: attrib={'xlink:href': '#%s' % glyph_id} if xposition != 0.0: attrib['x'] = short_float_fmt(xposition) if yposition != 0.0: attrib['y'] = short_float_fmt(yposition) writer.element( 'use', attrib=attrib) writer.end('g') else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: writer.start('defs') for char_id, glyph_path in six.iteritems(glyph_map_new): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): path_data = "" else: path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} font_scale = fontsize / text2path.FONT_SCALE attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,)), ('scale', (font_scale, -font_scale))]) writer.start('g', attrib=attrib) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element( 'use', transform=generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), attrib={'xlink:href': '#%s' % char_id}) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element('path', d=path_data) writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = short_float_fmt(gc.get_alpha()) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) fontsize = prop.get_size_in_points() fontfamily = font.family_name fontstyle = prop.get_style() attrib = {} # Must add "px" to workaround a Firefox bug style['font-size'] = short_float_fmt(fontsize) + 'px' style['font-family'] = six.text_type(fontfamily) style['font-style'] = prop.get_style().lower() style['font-weight'] = six.text_type(prop.get_weight()).lower() attrib['style'] = generate_css(style) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original # coordinates and add alignment information. # Get anchor coordinates. transform = mtext.get_transform() ax, ay = transform.transform_point(mtext.get_position()) ay = self.height - ay # Don't do vertical anchor alignment. Most applications do not # support 'alignment-baseline' yet. Apply the vertical layout # to the anchor point manually for now. angle_rad = angle * np.pi / 180. dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] ha_mpl_to_svg = {'left': 'start', 'right': 'end', 'center': 'middle'} style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] attrib['x'] = short_float_fmt(ax) attrib['y'] = short_float_fmt(ay) attrib['style'] = generate_css(style) attrib['transform'] = "rotate(%s, %s, %s)" % ( short_float_fmt(-angle), short_float_fmt(ax), short_float_fmt(ay)) writer.element('text', s, attrib=attrib) else: attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]) writer.element('text', s, attrib=attrib) if rcParams['svg.fonttype'] == 'svgfont': fontset = self._fonts.setdefault(font.fname, set()) for c in s: fontset.add(ord(c)) else: writer.comment(s) width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]) # Apply attributes to 'g', not 'text', because we likely # have some rectangles as well with the same style and # transformation writer.start('g', attrib=attrib) writer.start('text') # Sort the characters by font, and output one tspan for # each spans = OrderedDict() for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css({ 'font-size': short_float_fmt(fontsize) + 'px', 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'font-weight': font.style_name.lower()}) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) if rcParams['svg.fonttype'] == 'svgfont': for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) for style, chars in six.iteritems(spans): chars.sort() same_y = True if len(chars) > 1: last_y = chars[0][1] for i in xrange(1, len(chars)): if chars[i][1] != last_y: same_y = False break if same_y: ys = six.text_type(chars[0][1]) else: ys = ' '.join(six.text_type(c[1]) for c in chars) attrib = { 'style': style, 'x': ' '.join(short_float_fmt(c[0]) for c in chars), 'y': ys } writer.element( 'tspan', ''.join(unichr(c[2]) for c in chars), attrib=attrib) writer.end('text') if len(svg_rects): for x, y, width, height in svg_rects: writer.element( 'rect', x=short_float_fmt(x), y=short_float_fmt(-y + height), width=short_float_fmt(width), height=short_float_fmt(height) ) writer.end('g') def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start( 'g', attrib={'clip-path': 'url(#%s)' % clipid}) if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) if rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): return self._text2path.get_text_width_height_descent(s, prop, ismath)
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width=width self.height=height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._n_gradients = 0 self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog%(width,height,width,height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid if gc.get_url() is not None: self._svgwriter.write('<a xlink:href="%s">' % gc.get_url()) style = self._get_style(gc, rgbFace) self._svgwriter.write ('<%s style="%s" %s %s/>\n' % ( element, style, clippath, details)) if gc.get_url() is not None: self._svgwriter.write('</a>') def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ HATCH_SIZE = 72 dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) id = self._hatchd.get(dictkey) if id is None: id = 'h%s' % md5(str(dictkey)).hexdigest() self._svgwriter.write('<defs>\n <pattern id="%s" ' % id) self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ') self._svgwriter.write(' width="%d" height="%d" >\n' % (HATCH_SIZE, HATCH_SIZE)) path_data = self._convert_path( gc.get_hatch_path(), Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) self._svgwriter.write( '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (HATCH_SIZE+1, HATCH_SIZE+1, fill)) path = '<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % ( path_data, rgb2hex(gc.get_rgb()[:3]), rgb2hex(gc.get_rgb()[:3])) self._svgwriter.write(path) self._svgwriter.write('\n </pattern>\n</defs>') self._hatchd[dictkey] = id return id def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if gc.get_hatch() is not None: fill = "url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace[:3]) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % ( ','.join(['%f'%val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %f; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % ( fill, rgb2hex(gc.get_rgb()[:3]), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %f' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) path_data = self._convert_path(clippath, clippath_trans, simplify=False) path = '<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) path = '<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals() else: return None id = self._clipd.get(path) if id is None: id = 'p%s' % md5(path).hexdigest() self._svgwriter.write('<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write('\n </clipPath>\n</defs>') self._clipd[path] = id return id def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self._svgwriter.write('<g id="%s">\n' % (gid)) else: self._groupd[s] = self._groupd.get(s,0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] _path_commands = { Path.MOVETO: 'M%f %f', Path.LINETO: 'L%f %f', Path.CURVE3: 'Q%f %f %f %f', Path.CURVE4: 'C%f %f %f %f %f %f' } def _make_flip_transform(self, transform): return (transform + Affine2D() .scale(1.0, -1.0) .translate(0.0, self.height)) def _convert_path(self, path, transform, clip=False, simplify=None): path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None for points, code in path.iter_segments(transform, clip=clip, simplify=simplify): if code == Path.CLOSEPOLY: segment = 'z' else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender("\n") currpos = 0 appender(segment) currpos += len(segment) return ''.join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path( path, trans_and_flip, clip=clip, simplify=simplify) self._draw_svg_element('path', 'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) name = self._markers.get(key) if name is None: name = 'm%s' % md5(key).hexdigest() write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>' % clippath) trans_and_flip = self._make_flip_transform(trans) for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write ('<use style="%s" %s/>\n' % (style, details)) write('</g>') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._svgwriter.write path_codes = [] write('<defs>\n') for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) name = 'coll%x_%x_%s' % (self._path_collection_id, i, md5(d).hexdigest()) write('<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write('</defs>\n') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): clipid = self._get_gc_clip_svg(gc0) url = gc0.get_url() if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) if clipid is not None: write('<g clip-path="url(#%s)">' % clipid) details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc0, rgbFace) self._svgwriter.write ('<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write('</g>') if url is not None: self._svgwriter.write('</a>') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) write = self._svgwriter.write write('<defs>') for i in range(3): x1, y1 = points[i] x2, y2 = points[(i + 1) % 3] x3, y3 = points[(i + 2) % 3] c = colors[i][:3] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 write('<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">' % (self._n_gradients, i, x1, y1, xb, yb)) write('<stop offset="0" stop-color="%s" stop-opacity="1.0"/>' % rgb2hex(c)) write('<stop offset="1" stop-color="%s" stop-opacity="0.0"/>' % rgb2hex(c)) write('</linearGradient>') # Define the triangle itself as a "def" since we use it 4 times write('<polygon id="GT%x" points="%f %f %f %f %f %f"/>' % (self._n_gradients, x1, y1, x2, y2, x3, y3)) write('</defs>\n') avg_color = np.sum(colors[:, :3], axis=0) / 3.0 write('<use xlink:href="#GT%x" fill="%s"/>\n' % (self._n_gradients, rgb2hex(avg_color))) for i in range(3): write('<use xlink:href="#GT%x" fill="url(#GR%x_%d)" filter="url(#colorAdd)"/>\n' % (self._n_gradients, self._n_gradients, i)) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): write = self._svgwriter.write clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>\n' % clippath) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) write('</g>\n') def draw_image(self, gc, x, y, im): # MGDTODO: Support clippath here trans = [1,0,0,1,0,0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] transstr = 'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows,numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h,w = im.get_size_out() url = getattr(im, '_url', None) if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) self._svgwriter.write ( '<image x="%f" y="%f" width="%f" height="%f" ' '%s xlink:href="'%(x/trans[0], (self.height-y)/trans[3]-h, w, h, transstr) ) if rcParams['svg.image_inline']: self._svgwriter.write("data:image/png;base64,\n") stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() self._svgwriter.write(base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename,0) + 1 filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename]) verbose.report( 'Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write('"/>\n') if url is not None: self._svgwriter.write('</a>') def _adjust_char_id(self, char_id): return char_id.replace("%20","_") def draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ # this method works for normal text, mathtext and usetex mode. # But currently only utilized by draw_tex method. glyph_map=self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()[:3]) fontsize = prop.get_size_in_points() write = self._svgwriter.write if ismath == False: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font(font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs _flip = Affine2D().scale(1.0, -1.0) if glyph_map_new: write('<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): path = Path(*glyph_path) path_data = self._convert_path(path, _flip, simplify=False) path_element = '<path id="%s" d="%s"/>\n' % (char_id, ''.join(path_data)) write(path_element) write('</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / text2path.FONT_SCALE)) for glyph_id, xposition, yposition, scale in glyph_info: svg.append('<use xlink:href="#%s"' % glyph_id) svg.append(' x="%f" y="%f"' % (xposition, yposition)) #(currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map) else: _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: write('<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): char_id = self._adjust_char_id(char_id) path = Path(*glyph_path) path_data = self._convert_path(path, None, simplify=False) #_flip) path_element = '<path id="%s" d="%s"/>\n' % (char_id, ''.join(path_data)) write(path_element) write('</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f,-%f)">\n' % (fontsize / text2path.FONT_SCALE, fontsize / text2path.FONT_SCALE)) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) svg.append('<use xlink:href="#%s"' % char_id) svg.append(' x="%f" y="%f" transform="scale(%f)"' % (xposition/scale, yposition/scale, scale)) svg.append('/>\n') for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, None, simplify=False) path_element = '<path d="%s"/>\n' % (''.join(path_data)) svg.append(path_element) svg.append('</g><!-- style -->\n') if clipid is not None: svg.append('</g><!-- clipid -->\n') svg = ''.join(svg) write(svg) def draw_tex(self, gc, x, y, s, prop, angle): self.draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' x="%f"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ('font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f' % (fontsize, fontfamily,fontstyle, color, gc.get_alpha())) if angle!=0: transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append("c%f %f %f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = ''.join(path_data) char_num = 'c_%s' % md5(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write style = "fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append('<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append(' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) ) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x,curr_y = 0.0,0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = - new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%f,%f) rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append('<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self.open_group("mathtext") write (''.join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: size = prop.get_size_in_points() texmanager = self._text2path.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class RendererPS(_backend_pdf_ps.RendererPDFPSBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ _afm_font_dir = cbook._get_data_path("fonts/afm") _use_afm_rc_name = "ps.useafm" def __init__(self, width, height, pswriter, imagedpi=72): # Although postscript itself is dpi independent, we need to inform the # image code about a requested dpi to generate high resolution images # and them scale them before embedding them. super().__init__(width, height) self._pswriter = pswriter if mpl.rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self._hatches = {} self.image_magnification = imagedpi / 72 self._clip_paths = {} self._path_collection_id = 0 self._character_tracker = _backend_pdf_ps.CharacterTracker() self.mathtext_parser = MathTextParser("PS") @cbook.deprecated("3.3") @property def used_characters(self): return self._character_tracker.used_characters @cbook.deprecated("3.3") def track_characters(self, *args, **kwargs): """Keep track of which characters are required from each font.""" self._character_tracker.track(*args, **kwargs) @cbook.deprecated("3.3") def merge_used_characters(self, *args, **kwargs): self._character_tracker.merge(*args, **kwargs) def set_color(self, r, g, b, store=True): if (r, g, b) != self.color: if r == g and r == b: self._pswriter.write("%1.3f setgray\n" % r) else: self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n" % (r, g, b)) if store: self.color = (r, g, b) def set_linewidth(self, linewidth, store=True): linewidth = float(linewidth) if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n" % linewidth) if store: self.linewidth = linewidth def set_linejoin(self, linejoin, store=True): if linejoin != self.linejoin: self._pswriter.write("%d setlinejoin\n" % linejoin) if store: self.linejoin = linejoin def set_linecap(self, linecap, store=True): if linecap != self.linecap: self._pswriter.write("%d setlinecap\n" % linecap) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=True): if self.linedash is not None: oldo, oldseq = self.linedash if np.array_equal(seq, oldseq) and oldo == offset: return if seq is not None and len(seq): s = "[%s] %d setdash\n" % (_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset, seq) def set_font(self, fontname, fontsize, store=True): if mpl.rcParams['ps.useafm']: return if (fontname, fontsize) != (self.fontname, self.fontsize): out = ("/%s findfont\n" "%1.3f scalefont\n" "setfont\n" % (fontname, fontsize)) self._pswriter.write(out) if store: self.fontname = fontname self.fontsize = fontsize def create_hatch(self, hatch): sidelen = 72 if hatch in self._hatches: return self._hatches[hatch] name = 'H%d' % len(self._hatches) linewidth = mpl.rcParams['hatch.linewidth'] pageheight = self.height * 72 self._pswriter.write(f"""\ << /PatternType 1 /PaintType 2 /TilingType 2 /BBox[0 0 {sidelen:d} {sidelen:d}] /XStep {sidelen:d} /YStep {sidelen:d} /PaintProc {{ pop {linewidth:f} setlinewidth {self._convert_path( Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)} gsave fill grestore stroke }} bind >> matrix 0.0 {pageheight:f} translate makepattern /{name} exch def """) self._hatches[hatch] = name return name def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] imagecmd = "false 3 colorimage" data = im[::-1, :, :3] # Vertically flipped rgb values. # data.tobytes().hex() has no spaces, so can be linewrapped by simply # splitting data every nchars. It's equivalent to textwrap.fill only # much faster. nchars = 128 data = data.tobytes().hex() hexlines = "\n".join([ data[n * nchars:(n + 1) * nchars] for n in range(math.ceil(len(data) / nchars)) ]) if transform is None: matrix = "1 0 0 1 0 0" xscale = w / self.image_magnification yscale = h / self.image_magnification else: matrix = " ".join(map(str, transform.frozen().to_values())) xscale = 1.0 yscale = 1.0 bbox = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() clip = [] if bbox is not None: clipx, clipy, clipw, cliph = bbox.bounds clip.append('%s clipbox' % _nums_to_str(clipw, cliph, clipx, clipy)) if clippath is not None: id = self._get_clip_path(clippath, clippath_trans) clip.append('%s' % id) clip = '\n'.join(clip) self._pswriter.write(f"""\ gsave {clip} {x:f} {y:f} translate [{matrix}] concat {xscale:f} {yscale:f} scale /DataString {w:d} string def {w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ] {{ currentfile DataString readhexstring pop }} bind {imagecmd} {hexlines} grestore """) def _convert_path(self, path, transform, clip=False, simplify=None): if clip: clip = (0.0, 0.0, self.width * 72.0, self.height * 72.0) else: clip = None return _path.convert_to_string(path, transform, clip, simplify, None, 6, [b'm', b'l', b'', b'c', b'cl'], True).decode('ascii') def _get_clip_path(self, clippath, clippath_transform): key = (clippath, id(clippath_transform)) pid = self._clip_paths.get(key) if pid is None: pid = 'c%x' % len(self._clip_paths) clippath_bytes = self._convert_path(clippath, clippath_transform, simplify=False) self._pswriter.write(f"""\ /{pid} {{ {clippath_bytes} clip newpath }} bind def """) self._clip_paths[key] = pid return pid def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited clip = rgbFace is None and gc.get_hatch_path() is None simplify = path.should_simplify and clip ps = self._convert_path(path, transform, clip=clip, simplify=simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited if debugPS: self._pswriter.write('% draw_markers \n') ps_color = (None if _is_transparent(rgbFace) else '%1.3f setgray' % rgbFace[0] if rgbFace[0] == rgbFace[1] == rgbFace[2] else '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace[:3]) # construct the generic marker command: # don't want the translate to be global ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] lw = gc.get_linewidth() alpha = (gc.get_alpha() if gc.get_forced_alpha() or len(gc.get_rgb()) == 3 else gc.get_rgb()[3]) stroke = lw > 0 and alpha > 0 if stroke: ps_cmd.append('%.1f setlinewidth' % lw) jint = gc.get_joinstyle() ps_cmd.append('%d setlinejoin' % jint) cint = gc.get_capstyle() ps_cmd.append('%d setlinecap' % cint) ps_cmd.append( self._convert_path(marker_path, marker_trans, simplify=False)) if rgbFace: if stroke: ps_cmd.append('gsave') if ps_color: ps_cmd.extend([ps_color, 'fill']) if stroke: ps_cmd.append('grestore') if stroke: ps_cmd.append('stroke') ps_cmd.extend(['grestore', '} bind def']) for vertices, code in path.iter_segments(trans, clip=(0, 0, self.width * 72, self.height * 72), simplify=False): if len(vertices): x, y = vertices[-2:] ps_cmd.append("%g %g o" % (x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 2) * uses_per_path # cost of definition+use is # (len_path + 3) + 3 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) should_do_optimization = \ len_path + 3 * uses_per_path + 3 < (len_path + 2) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) write = self._pswriter.write path_codes = [] for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): name = 'p%x_%x' % (self._path_collection_id, i) path_bytes = self._convert_path(path, transform, simplify=False) write(f"""\ /{name} {{ newpath translate {path_bytes} }} bind def """) path_codes.append(name) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc0, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # docstring inherited w, h, bl = self.get_text_width_height_descent(s, prop, ismath) fontsize = prop.get_size_in_points() thetext = 'psmarker%d' % self.textcnt color = '%1.3f,%1.3f,%1.3f' % gc.get_rgb()[:3] fontcmd = { 'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}' }.get(mpl.rcParams['font.family'][0], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) corr = 0 # w/2*(fontsize-10)/10 if mpl.rcParams['text.latex.preview']: # use baseline alignment! pos = _nums_to_str(x - corr, y) self.psfrag.append( r'\psfrag{%s}[Bl][Bl][1][%f]{\fontsize{%f}{%f}%s}' % (thetext, angle, fontsize, fontsize * 1.25, tex)) else: # Stick to the bottom alignment. pos = _nums_to_str(x - corr, y - bl) self.psfrag.append( r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % (thetext, angle, fontsize, fontsize * 1.25, tex)) self._pswriter.write(f"""\ gsave {pos} moveto ({thetext}) show grestore """) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited # local to avoid repeated attribute lookups write = self._pswriter.write if debugPS: write("% text\n") if _is_transparent(gc.get_rgb()): return # Special handling for fully transparent. if ismath == 'TeX': return self.draw_tex(gc, x, y, s, prop, angle) elif ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) elif mpl.rcParams['ps.useafm']: self.set_color(*gc.get_rgb()) font = self._get_font_afm(prop) fontname = font.get_fontname() fontsize = prop.get_size_in_points() scale = 0.001 * fontsize thisx = 0 thisy = font.get_str_bbox_and_descent(s)[4] * scale last_name = None lines = [] for c in s: name = uni2type1.get(ord(c), f"uni{ord(c):04X}") try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') if last_name is not None: kern = font.get_kern_dist_from_name(last_name, name) else: kern = 0 last_name = name thisx += kern * scale lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name)) thisx += width * scale thetext = "\n".join(lines) self._pswriter.write(f"""\ gsave /{fontname} findfont {fontsize} scalefont setfont {x:f} {y:f} translate {angle:f} rotate {thetext} grestore """) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self._character_tracker.track(font, s) self.set_color(*gc.get_rgb()) ps_name = (font.postscript_name.encode('ascii', 'replace').decode('ascii')) self.set_font(ps_name, prop.get_size_in_points()) thetext = '\n'.join( '%f 0 m /%s glyphshow' % (x, font.get_glyph_name(glyph_idx)) for glyph_idx, x in _text_layout.layout(s, font)) self._pswriter.write(f"""\ gsave {x:f} {y:f} translate {angle:f} rotate {thetext} grestore """) def new_gc(self): # docstring inherited return GraphicsContextPS() def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw the math text using matplotlib.mathtext.""" if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) self._character_tracker.merge(used_characters) self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() self._pswriter.write(f"""\ gsave {x:f} {y:f} translate {angle:f} rotate {thetext} grestore """) def draw_gouraud_triangle(self, gc, points, colors, trans): self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), colors.reshape((1, 3, 4)), trans) def draw_gouraud_triangles(self, gc, points, colors, trans): assert len(points) == len(colors) assert points.ndim == 3 assert points.shape[1] == 3 assert points.shape[2] == 2 assert colors.ndim == 3 assert colors.shape[1] == 3 assert colors.shape[2] == 4 shape = points.shape flat_points = points.reshape((shape[0] * shape[1], 2)) flat_points = trans.transform(flat_points) flat_colors = colors.reshape((shape[0] * shape[1], 4)) points_min = np.min(flat_points, axis=0) - (1 << 12) points_max = np.max(flat_points, axis=0) + (1 << 12) factor = np.ceil((2**32 - 1) / (points_max - points_min)) xmin, ymin = points_min xmax, ymax = points_max streamarr = np.empty(shape[0] * shape[1], dtype=[('flags', 'u1'), ('points', '2>u4'), ('colors', '3u1')]) streamarr['flags'] = 0 streamarr['points'] = (flat_points - points_min) * factor streamarr['colors'] = flat_colors[:, :3] * 255.0 stream = quote_ps_string(streamarr.tobytes()) self._pswriter.write(f"""\ gsave << /ShadingType 4 /ColorSpace [/DeviceRGB] /BitsPerCoordinate 32 /BitsPerComponent 8 /BitsPerFlag 8 /AntiAlias true /Decode [ {xmin:f} {xmax:f} {ymin:f} {ymax:f} 0 1 0 1 0 1 ] /DataSource ({stream}) >> shfill grestore """) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript snippet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% " + command + "\n") mightstroke = (gc.get_linewidth() > 0 and not _is_transparent(gc.get_rgb())) if not mightstroke: stroke = False if _is_transparent(rgbFace): fill = False hatch = gc.get_hatch() if mightstroke: self.set_linewidth(gc.get_linewidth()) jint = gc.get_joinstyle() self.set_linejoin(jint) cint = gc.get_capstyle() self.set_linecap(cint) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') cliprect = gc.get_clip_rectangle() if cliprect: x, y, w, h = cliprect.bounds write('%1.4g %1.4g %1.4g %1.4g clipbox\n' % (w, h, x, y)) clippath, clippath_trans = gc.get_clip_path() if clippath: id = self._get_clip_path(clippath, clippath_trans) write('%s\n' % id) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke or hatch: write("gsave\n") self.set_color(*rgbFace[:3], store=False) write("fill\n") if stroke or hatch: write("grestore\n") if hatch: hatch_name = self.create_hatch(hatch) write("gsave\n") write("%f %f %f " % gc.get_hatch_color()[:3]) write("%s setpattern fill grestore\n" % hatch_name) if stroke: write("stroke\n") write("grestore\n")
class TextToPath(object): """A class that converts strings to paths.""" FONT_SCALE = 100. DPI = 72 def __init__(self): self.mathtext_parser = MathTextParser('path') self._texmanager = None @property @cbook.deprecated("3.0") def tex_font_map(self): return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) def _get_font(self, prop): """ Find the `FT2Font` matching font properties *prop*, with its size set. """ fname = font_manager.findfont(prop) font = get_font(fname) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode)) def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode)) return char_id @cbook.deprecated( "3.1", alternative="font.get_path() and manual translation of the vertices") def glyph_to_path(self, font, currx=0.): """Convert the *font*'s current glyph to a (vertices, codes) pair.""" verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = fontsize / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False, usetex=False): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). Parameters ---------- prop : `matplotlib.font_manager.FontProperties` instance The font properties for the text. s : str The text to be converted. usetex : bool, optional Whether to use tex rendering. Defaults to ``False``. ismath : bool, optional If True, use mathtext parser. Effective only if ``usetex == False``. Returns ------- verts, codes : tuple of lists *verts* is a list of numpy arrays containing the x and y coordinates of the vertices. *codes* is a list of path codes. Examples -------- Create a list of vertices and codes from a text, and create a `Path` from those:: from matplotlib.path import Path from matplotlib.textpath import TextToPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Humor Sans", style="italic") verts, codes = TextToPath().get_text_path(fp, "ABC") path = Path(verts, codes, closed=False) Also see `TextPath` for a more direct way to create a path from a text. """ if not usetex: if not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font( font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext( prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ Convert string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = glyph.linearHoriAdvance / 65536 char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: glyph_map_new[char_id] = font.get_path() currx += kern / 64 xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Parse mathtext string *s* and convert it to a (vertices, codes) pair. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Process string *s* with usetex and convert it to a (vertices, codes) pair. """ # Implementation mostly borrowed from pdf backend. dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) with dviread.Dvi(dvifile, self.DPI) as dvi: page, = dvi if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. for x1, y1, dvifont, glyph, width in page.text: font, enc = self._get_ps_font_and_encoding(dvifont.texname) char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) if enc: charcode = enc.get(glyph, None) else: charcode = glyph ft2font_flag = LOAD_TARGET_LIGHT if charcode is not None: glyph0 = font.load_char(charcode, flags=ft2font_flag) else: _log.warning( "The glyph (%d) of font (%s) cannot be " "converted with the encoding. Glyph may " "be wrong.", glyph, font.fname) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) font_bunch = tex_font_map[texname] if font_bunch.filename is None: raise ValueError(("No usable font file found for %s (%s). " "The font may lack a Type-1 version.") % (font_bunch.psname, texname)) font = get_font(font_bunch.filename) for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451), ("ADOBE_STANDARD", 1094995778)]: try: font.select_charmap(charmap_code) except (ValueError, RuntimeError): pass else: break else: charmap_name = "" _log.warning("No supported encoding in font (%s).", font_bunch.filename) if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding: enc0 = dviread.Encoding(font_bunch.encoding) enc = { i: _get_adobe_standard_encoding().get(c, None) for i, c in enumerate(enc0.encoding) } else: enc = {} return font, enc
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ # we want to cache the fonts at the class level so that when # multiple figures are created we can reuse them. This helps with # a bug on windows where the creation of too many figures leads to # too many open file handles. However, storing them at the class # level is not thread safe. The solution here is to let the # FigureCanvas acquire a lock on the fontd at the start of the # draw, and release it when it is done. This allows multiple # renderers to share the cached fonts, but only one figure can # draw at time and so the font cache is used by only one # renderer at a time. lock = threading.RLock() def __init__(self, width, height, dpi): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self._renderer = _RendererAgg(int(width), int(height), dpi) self._filter_renderers = [] self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) def __getstate__(self): # We only want to preserve the init keywords of the Renderer. # Anything else can be re-created. return {'width': self.width, 'height': self.height, 'dpi': self.dpi} def __setstate__(self, state): self.__init__(state['width'], state['height'], state['dpi']) def _update_methods(self): self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.copy_from_bbox = self._renderer.copy_from_bbox self.get_content_extents = self._renderer.get_content_extents def tostring_rgba_minimized(self): extents = self.get_content_extents() bbox = [[extents[0], self.height - (extents[1] + extents[3])], [extents[0] + extents[2], self.height - extents[1]]] region = self.copy_from_bbox(bbox) return np.array(region), extents def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = np.ceil(npts / nmax) chsize = int(np.ceil(npts / nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1, :] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) try: self._renderer.draw_path(gc, p, transform, rgbFace) except OverflowError: raise OverflowError("Exceeded cell block limit (set " "'agg.path.chunksize' rcparam)") else: try: self._renderer.draw_path(gc, path, transform, rgbFace) except OverflowError: raise OverflowError("Exceeded cell block limit (set " "'agg.path.chunksize' rcparam)") def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) xd = descent * sin(radians(angle)) yd = descent * cos(radians(angle)) x = np.round(x + ox + xd) y = np.round(y - oy + yd) self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=flags) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) d = font.get_descent() / 64.0 # The descent needs to be adjusted for the angle. xo, yo = font.get_bitmap_offset() xo /= 64.0 yo /= 64.0 xd = -d * sin(radians(angle)) yd = d * cos(radians(angle)) self._renderer.draw_text_image( font, np.round(x - xd + xo), np.round(y + yd + yo) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath in ["TeX", "TeX!"]: # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent( s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) w, h = font.get_width_height() # width and height of unrotated string d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # docstring inherited # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) w, h, d = self.get_text_width_height_descent(s, prop, ismath) xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = np.round(x + xd) y = np.round(y + yd) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): # docstring inherited return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, caching for efficiency """ fname = findfont(prop) font = get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): # docstring inherited return points * self.dpi / 72 def tostring_rgb(self): return self._renderer.tostring_rgb() def tostring_argb(self): return self._renderer.tostring_argb() def buffer_rgba(self): return self._renderer.buffer_rgba() def clear(self): self._renderer.clear() def option_image_nocomposite(self): # docstring inherited # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): # docstring inherited return False def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), ... xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy # The incoming data is float, but the _renderer type-checking wants # to see integers. self._renderer.restore_region(region, int(x1), int(y1), int(x2), int(y2), int(ox), int(oy)) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ width, height = int(self.width), int(self.height) buffer, (l, b, w, h) = self.tostring_rgba_minimized() self._renderer = self._filter_renderers.pop() self._update_methods() if w > 0 and h > 0: img = np.frombuffer(buffer, np.uint8) img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., self.dpi) gc = self.new_gc() if img.dtype.kind == 'f': img = np.asarray(img * 255., np.uint8) img = img[::-1] self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
class RendererHTMLCanvas(RendererBase): def __init__(self, ctx, width, height, dpi, fig): super().__init__() self.fig = fig self.ctx = ctx self.width = width self.height = height self.ctx.width = self.width self.ctx.height = self.height self.dpi = dpi self.fontd = maxdict(50) self.mathtext_parser = MathTextParser("bitmap") def new_gc(self): return GraphicsContextHTMLCanvas(renderer=self) def points_to_pixels(self, points): return (points / 72.0) * self.dpi def _matplotlib_color_to_CSS(self, color, alpha, alpha_overrides, is_RGB=True): if not is_RGB: R, G, B, alpha = colorConverter.to_rgba(color) color = (R, G, B) if (len(color) == 4) and (alpha is None): alpha = color[3] if alpha is None: CSS_color = rgb2hex(color[:3]) else: R = int(color[0] * 255) G = int(color[1] * 255) B = int(color[2] * 255) if len(color) == 3 or alpha_overrides: CSS_color = f"""rgba({R:d}, {G:d}, {B:d}, {alpha:.3g})""" else: CSS_color = """rgba({:d}, {:d}, {:d}, {:.3g})""".format( R, G, B, color[3]) return CSS_color def _set_style(self, gc, rgbFace=None): if rgbFace is not None: self.ctx.fillStyle = self._matplotlib_color_to_CSS( rgbFace, gc.get_alpha(), gc.get_forced_alpha()) if gc.get_capstyle(): self.ctx.lineCap = _capstyle_d[gc.get_capstyle()] self.ctx.strokeStyle = self._matplotlib_color_to_CSS( gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha()) self.ctx.lineWidth = self.points_to_pixels(gc.get_linewidth()) def _path_helper(self, ctx, path, transform, clip=None): ctx.beginPath() for points, code in path.iter_segments(transform, remove_nans=True, clip=clip): if code == Path.MOVETO: ctx.moveTo(points[0], points[1]) elif code == Path.LINETO: ctx.lineTo(points[0], points[1]) elif code == Path.CURVE3: ctx.quadraticCurveTo(*points) elif code == Path.CURVE4: ctx.bezierCurveTo(*points) elif code == Path.CLOSEPOLY: ctx.closePath() def draw_path(self, gc, path, transform, rgbFace=None): self._set_style(gc, rgbFace) if rgbFace is None and gc.get_hatch() is None: figure_clip = (0, 0, self.width, self.height) else: figure_clip = None transform += Affine2D().scale(1, -1).translate(0, self.height) self._path_helper(self.ctx, path, transform, figure_clip) if rgbFace is not None: self.ctx.fill() self.ctx.fillStyle = "#000000" if gc.stroke: self.ctx.stroke() def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) def draw_image(self, gc, x, y, im, transform=None): im = np.flipud(im) h, w, d = im.shape y = self.ctx.height - y - h im = np.ravel(np.uint8(np.reshape(im, (h * w * d, -1)))).tobytes() pixels_proxy = create_proxy(im) pixels_buf = pixels_proxy.getBuffer("u8clamped") img_data = ImageData.new(pixels_buf.data, w, h) self.ctx.save() in_memory_canvas = document.createElement("canvas") in_memory_canvas.width = w in_memory_canvas.height = h in_memory_canvas_context = in_memory_canvas.getContext("2d") in_memory_canvas_context.putImageData(img_data, 0, 0) self.ctx.drawImage(in_memory_canvas, x, y, w, h) self.ctx.restore() pixels_proxy.destroy() pixels_buf.release() def _get_font(self, prop): key = hash(prop) font_value = self.fontd.get(key) if font_value is None: fname = findfont(prop) font_value = self.fontd.get(fname) if font_value is None: font = FT2Font(str(fname)) font_file_name = fname[fname.rfind("/") + 1:] font_value = font, font_file_name self.fontd[fname] = font_value self.fontd[key] = font_value font, font_file_name = font_value font.clear() font.set_size(prop.get_size_in_points(), self.dpi) return font, font_file_name def get_text_width_height_descent(self, s, prop, ismath): if ismath: image, d = self.mathtext_parser.parse(s, self.dpi, prop) w, h = image.get_width(), image.get_height() else: font, _ = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 h /= 64.0 d = font.get_descent() / 64.0 return w, h, d def _draw_math_text(self, gc, x, y, s, prop, angle): rgba, descent = self.mathtext_parser.to_rgba(s, gc.get_rgb(), self.dpi, prop.get_size_in_points()) height, width, _ = rgba.shape angle = math.radians(angle) if angle != 0: self.ctx.save() self.ctx.translate(x, y) self.ctx.rotate(-angle) self.ctx.translate(-x, -y) self.draw_image(gc, x, -y - descent, np.flipud(rgba)) if angle != 0: self.ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def _load_font_into_web(loaded_face): document.fonts.add(loaded_face) window.font_counter += 1 self.fig.draw_idle() if ismath: self._draw_math_text(gc, x, y, s, prop, angle) return angle = math.radians(angle) width, height, descent = self.get_text_width_height_descent( s, prop, ismath) x -= math.sin(angle) * descent y -= math.cos(angle) * descent - self.ctx.height font_size = self.points_to_pixels(prop.get_size_in_points()) _, font_file_name = self._get_font(prop) font_face_arguments = ( prop.get_name(), f"url({_base_fonts_url + font_file_name})", ) # The following snippet loads a font into the browser's # environment if it wasn't loaded before. This check is necessary # to help us avoid loading the same font multiple times. Further, # it helps us to avoid the infinite loop of # load font --> redraw --> load font --> redraw --> .... if font_face_arguments not in _font_set: _font_set.add(font_face_arguments) f = FontFace.new(*font_face_arguments) f.load().then(_load_font_into_web) font_property_string = "{} {} {:.3g}px {}, {}".format( prop.get_style(), prop.get_weight(), font_size, prop.get_name(), prop.get_family()[0], ) if angle != 0: self.ctx.save() self.ctx.translate(x, y) self.ctx.rotate(-angle) self.ctx.translate(-x, -y) self.ctx.font = font_property_string self.ctx.fillStyle = self._matplotlib_color_to_CSS( gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha()) self.ctx.fillText(s, x, y) self.ctx.fillStyle = "#000000" if angle != 0: self.ctx.restore()
class RendererPS(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ fontd = maxdict(50) afmfontd = maxdict(50) def __init__(self, width, height, pswriter, imagedpi=72): """ Although postscript itself is dpi independent, we need to imform the image code about a requested dpi to generate high res images and them scale them before embeddin them """ RendererBase.__init__(self) self.width = width self.height = height self._pswriter = pswriter if rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi if rcParams['path.simplify']: self.simplify = (width * imagedpi, height * imagedpi) else: self.simplify = None # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self.hatch = None self.image_magnification = imagedpi/72.0 self._clip_paths = {} self._path_collection_id = 0 self.used_characters = {} self.mathtext_parser = MathTextParser("PS") def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" realpath, stat_key = get_realpath_and_stat(font.fname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update([ord(x) for x in s]) def merge_used_characters(self, other): for stat_key, (realpath, charset) in other.items(): used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update(charset) def set_color(self, r, g, b, store=1): if (r,g,b) != self.color: if r==g and r==b: self._pswriter.write("%1.3f setgray\n"%r) else: self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n"%(r,g,b)) if store: self.color = (r,g,b) def set_linewidth(self, linewidth, store=1): if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n"%linewidth) if store: self.linewidth = linewidth def set_linejoin(self, linejoin, store=1): if linejoin != self.linejoin: self._pswriter.write("%d setlinejoin\n"%linejoin) if store: self.linejoin = linejoin def set_linecap(self, linecap, store=1): if linecap != self.linecap: self._pswriter.write("%d setlinecap\n"%linecap) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=1): if self.linedash is not None: oldo, oldseq = self.linedash if seq_allequal(seq, oldseq): return if seq is not None and len(seq): s="[%s] %d setdash\n"%(_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset,seq) def set_font(self, fontname, fontsize, store=1): if rcParams['ps.useafm']: return if (fontname,fontsize) != (self.fontname,self.fontsize): out = ("/%s findfont\n" "%1.3f scalefont\n" "setfont\n" % (fontname,fontsize)) self._pswriter.write(out) if store: self.fontname = fontname if store: self.fontsize = fontsize def set_hatch(self, hatch): """ hatch can be one of: / - diagonal hatching \ - back diagonal | - vertical - - horizontal + - crossed X - crossed diagonal letters can be combined, in which case all the specified hatchings are done if same letter repeats, it increases the density of hatching in that direction """ hatches = {'horiz':0, 'vert':0, 'diag1':0, 'diag2':0} for letter in hatch: if (letter == '/'): hatches['diag2'] += 1 elif (letter == '\\'): hatches['diag1'] += 1 elif (letter == '|'): hatches['vert'] += 1 elif (letter == '-'): hatches['horiz'] += 1 elif (letter == '+'): hatches['horiz'] += 1 hatches['vert'] += 1 elif (letter.lower() == 'x'): hatches['diag1'] += 1 hatches['diag2'] += 1 def do_hatch(angle, density): if (density == 0): return "" return """\ gsave eoclip %s rotate 0.0 0.0 0.0 0.0 setrgbcolor 0 setlinewidth /hatchgap %d def pathbbox /hatchb exch def /hatchr exch def /hatcht exch def /hatchl exch def hatchl cvi hatchgap idiv hatchgap mul hatchgap hatchr cvi hatchgap idiv hatchgap mul {hatcht m 0 hatchb hatcht sub r } for stroke grestore """ % (angle, 12/density) self._pswriter.write("gsave\n") self._pswriter.write(do_hatch(90, hatches['horiz'])) self._pswriter.write(do_hatch(0, hatches['vert'])) self._pswriter.write(do_hatch(45, hatches['diag1'])) self._pswriter.write(do_hatch(-45, hatches['diag2'])) self._pswriter.write("grestore\n") def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop """ if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() l,b,r,t = texmanager.get_ps_bbox(s, fontsize) w = (r-l) h = (t-b) # TODO: We need a way to get a good baseline from # text.usetex return w, h, 0 if ismath: width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent if rcParams['ps.useafm']: if ismath: s = s[1:-1] font = self._get_font_afm(prop) l,b,w,h,d = font.get_str_bbox_and_descent(s) fontsize = prop.get_size_in_points() scale = 0.001*fontsize w *= scale h *= scale d *= scale return w, h, d font = self._get_font_ttf(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 #print s, w, h return w, h, d def flipy(self): 'return true if small y numbers are top for renderer' return False def _get_font_afm(self, prop): key = hash(prop) font = self.afmfontd.get(key) if font is None: fname = findfont(prop, fontext='afm') font = self.afmfontd.get(fname) if font is None: font = AFM(file(findfont(prop, fontext='afm'))) self.afmfontd[fname] = font self.afmfontd[key] = font return font def _get_font_ttf(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _rgba(self, im): return im.as_rgba_str() def _rgb(self, im): h,w,s = im.as_rgba_str() rgba = npy.fromstring(s, npy.uint8) rgba.shape = (h, w, 4) rgb = rgba[:,:,:3] return h, w, rgb.tostring() def _gray(self, im, rc=0.3, gc=0.59, bc=0.11): rgbat = im.as_rgba_str() rgba = npy.fromstring(rgbat[2], npy.uint8) rgba.shape = (rgbat[0], rgbat[1], 4) rgba_f = rgba.astype(npy.float32) r = rgba_f[:,:,0] g = rgba_f[:,:,1] b = rgba_f[:,:,2] gray = (r*rc + g*gc + b*bc).astype(npy.uint8) return rgbat[0], rgbat[1], gray.tostring() def _hex_lines(self, s, chars_per_line=128): s = binascii.b2a_hex(s) nhex = len(s) lines = [] for i in range(0,nhex,chars_per_line): limit = min(i+chars_per_line, nhex) lines.append(s[i:limit]) return lines def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): """ Draw the Image instance into the current axes; x is the distance in pixels from the left hand side of the canvas and y is the distance from bottom bbox is a matplotlib.transforms.BBox instance for clipping, or None """ im.flipud_out() if im.is_grayscale: h, w, bits = self._gray(im) imagecmd = "image" else: h, w, bits = self._rgb(im) imagecmd = "false 3 colorimage" hexlines = '\n'.join(self._hex_lines(bits)) xscale, yscale = ( w/self.image_magnification, h/self.image_magnification) figh = self.height*72 #print 'values', origin, flipud, figh, h, y clip = [] if bbox is not None: clipx,clipy,clipw,cliph = bbox.bounds clip.append('%s clipbox' % _nums_to_str(clipw, cliph, clipx, clipy)) if clippath is not None: id = self._get_clip_path(clippath, clippath_trans) clip.append('%s' % id) clip = '\n'.join(clip) #y = figh-(y+h) ps = """gsave %(clip)s %(x)s %(y)s translate %(xscale)s %(yscale)s scale /DataString %(w)s string def %(w)s %(h)s 8 [ %(w)s 0 0 -%(h)s 0 %(h)s ] { currentfile DataString readhexstring pop } bind %(imagecmd)s %(hexlines)s grestore """ % locals() self._pswriter.write(ps) # unflip im.flipud_out() def _convert_path(self, path, transform, simplify=None): path = transform.transform_path(path) ps = [] last_points = None for points, code in path.iter_segments(simplify): if code == Path.MOVETO: ps.append("%g %g m" % tuple(points)) elif code == Path.LINETO: ps.append("%g %g l" % tuple(points)) elif code == Path.CURVE3: points = quad2cubic(*(list(last_points[-2:]) + list(points))) ps.append("%g %g %g %g %g %g c" % tuple(points[2:])) elif code == Path.CURVE4: ps.append("%g %g %g %g %g %g c" % tuple(points)) elif code == Path.CLOSEPOLY: ps.append("cl") last_points = points ps = "\n".join(ps) return ps def _get_clip_path(self, clippath, clippath_transform): id = self._clip_paths.get((clippath, clippath_transform)) if id is None: id = 'c%x' % len(self._clip_paths) ps_cmd = ['/%s {' % id] ps_cmd.append(self._convert_path(clippath, clippath_transform)) ps_cmd.extend(['clip', 'newpath', '} bind def\n']) self._pswriter.write('\n'.join(ps_cmd)) self._clip_paths[(clippath, clippath_transform)] = id return id def draw_path(self, gc, path, transform, rgbFace=None): """ Draws a Path instance using the given affine transform. """ ps = self._convert_path(path, transform, self.simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): """ Draw the markers defined by path at each of the positions in x and y. path coordinates are points, x and y coords will be transformed by the transform """ if debugPS: self._pswriter.write('% draw_markers \n') write = self._pswriter.write if rgbFace: if rgbFace[0]==rgbFace[1] and rgbFace[0]==rgbFace[2]: ps_color = '%1.3f setgray' % rgbFace[0] else: ps_color = '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace # construct the generic marker command: ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] # dont want the translate to be global ps_cmd.append(self._convert_path(marker_path, marker_trans)) if rgbFace: ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore']) ps_cmd.extend(['stroke', 'grestore', '} bind def']) tpath = trans.transform_path(path) for vertices, code in tpath.iter_segments(): if len(vertices): x, y = vertices[-2:] ps_cmd.append("%g %g o" % (x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._pswriter.write path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): name = 'p%x_%x' % (self._path_collection_id, i) ps_cmd = ['/%s {' % name, 'newpath', 'translate'] ps_cmd.append(self._convert_path(path, transform)) ps_cmd.extend(['} bind def\n']) write('\n'.join(ps_cmd)) path_codes.append(name) for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'): """ draw a Text instance """ w, h, bl = self.get_text_width_height_descent(s, prop, ismath) fontsize = prop.get_size_in_points() corr = 0#w/2*(fontsize-10)/10 pos = _nums_to_str(x-corr, y) thetext = 'psmarker%d' % self.textcnt color = '%1.3f,%1.3f,%1.3f'% gc.get_rgb()[:3] fontcmd = {'sans-serif' : r'{\sffamily %s}', 'monospace' : r'{\ttfamily %s}'}.get( rcParams['font.family'], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) self.psfrag.append(r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}'%(thetext, angle, fontsize, fontsize*1.25, tex)) ps = """\ gsave %(pos)s moveto (%(thetext)s) show grestore """ % locals() self._pswriter.write(ps) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath): """ draw a Text instance """ # local to avoid repeated attribute lookups write = self._pswriter.write if debugPS: write("% text\n") if ismath=='TeX': return self.tex(gc, x, y, s, prop, angle) elif ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) elif isinstance(s, unicode): return self.draw_unicode(gc, x, y, s, prop, angle) elif rcParams['ps.useafm']: font = self._get_font_afm(prop) l,b,w,h = font.get_str_bbox(s) fontsize = prop.get_size_in_points() l *= 0.001*fontsize b *= 0.001*fontsize w *= 0.001*fontsize h *= 0.001*fontsize if angle==90: l,b = -b, l # todo generalize for arb rotations pos = _nums_to_str(x-l, y-b) thetext = '(%s)' % s fontname = font.get_fontname() fontsize = prop.get_size_in_points() rotate = '%1.1f rotate' % angle setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()[:3] #h = 0 ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(pos)s moveto %(rotate)s %(thetext)s %(setcolor)s show grestore """ % locals() self._draw_ps(ps, gc, None) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) write("%s m\n"%_nums_to_str(x,y)) if angle: write("gsave\n") write("%s rotate\n"%_num_to_str(angle)) descent = font.get_descent() / 64.0 if descent: write("0 %s rmoveto\n"%_num_to_str(descent)) write("(%s) show\n"%quote_ps_string(s)) if angle: write("grestore\n") def new_gc(self): return GraphicsContextPS() def draw_unicode(self, gc, x, y, s, prop, angle): """draw a unicode string. ps doesn't have unicode support, so we have to do this the hard way """ if rcParams['ps.useafm']: self.set_color(*gc.get_rgb()) font = self._get_font_afm(prop) fontname = font.get_fontname() fontsize = prop.get_size_in_points() scale = 0.001*fontsize thisx = 0 thisy = font.get_str_bbox_and_descent(s)[4] * scale last_name = None lines = [] for c in s: name = uni2type1.get(ord(c), 'question') try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') if last_name is not None: kern = font.get_kern_dist_from_name(last_name, name) else: kern = 0 last_name = name thisx += kern * scale lines.append('%f %f m /%s glyphshow'%(thisx, thisy, name)) thisx += width * scale thetext = "\n".join(lines) ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) cmap = font.get_charmap() lastgind = None #print 'text', s lines = [] thisx = 0 thisy = font.get_descent() / 64.0 for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') name = '.notdef' gind = 0 else: name = font.get_glyph_name(gind) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind thisx += kern/64.0 lines.append('%f %f m /%s glyphshow'%(thisx, thisy, name)) thisx += glyph.linearHoriAdvance/65536.0 thetext = '\n'.join(lines) ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) self.merge_used_characters(used_characters) self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript sniplet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% "+command+"\n") mightstroke = (gc.get_linewidth() > 0.0 and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0)) stroke = stroke and mightstroke fill = (fill and rgbFace is not None and (len(rgbFace) <= 3 or rgbFace[3] != 0.0)) if mightstroke: self.set_linewidth(gc.get_linewidth()) jint = gc.get_joinstyle() self.set_linejoin(jint) cint = gc.get_capstyle() self.set_linecap(cint) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') cliprect = gc.get_clip_rectangle() if cliprect: x,y,w,h=cliprect.bounds write('%1.4g %1.4g %1.4g %1.4g clipbox\n' % (w,h,x,y)) clippath, clippath_trans = gc.get_clip_path() if clippath: id = self._get_clip_path(clippath, clippath_trans) write('%s\n' % id) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke: write("gsave\n") self.set_color(store=0, *rgbFace[:3]) write("fill\ngrestore\n") else: self.set_color(store=0, *rgbFace[:3]) write("fill\n") hatch = gc.get_hatch() if hatch: self.set_hatch(hatch) if stroke: write("stroke\n") write("grestore\n")
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None, image_dpi=72): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self.image_dpi = image_dpi # the actual dpi we want to rasterize stuff with self._groupd = {} if not rcParams["svg.image_inline"]: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._has_gouraud = False self._n_gradients = 0 self._fonts = {} self.mathtext_parser = MathTextParser("SVG") RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( "svg", width="%ipt" % width, height="%ipt" % height, viewBox="0 0 %i %i" % (width, height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={"xmlns:xlink": "http://www.w3.org/1999/xlink"}, ) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) self.writer.flush() def _write_default_style(self): writer = self.writer default_style = generate_css({"stroke-linejoin": "round", "stroke-linecap": "butt"}) writer.start("defs") writer.start("style", type="text/css") writer.data("*{%s}\n" % default_style) writer.end("style") writer.end("defs") def _make_id(self, type, content): content = str(content) if six.PY3: content = content.encode("utf8") return "%s%s" % (type, md5(content).hexdigest()[:10]) def _make_flip_transform(self, transform): return transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height) def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(fname) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ if rgbFace is not None: rgbFace = tuple(rgbFace) edge = gc.get_rgb() if edge is not None: edge = tuple(edge) dictkey = (gc.get_hatch(), rgbFace, edge) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id("h", dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start("defs") for ((path, face, stroke), oid) in six.itervalues(self._hatchd): writer.start( "pattern", id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=six.text_type(HATCH_SIZE), height=six.text_type(HATCH_SIZE), ) path_data = self._convert_path( path, Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), simplify=False ) if face is None: fill = "none" else: fill = rgb2hex(face) writer.element( "rect", x="0", y="0", width=six.text_type(HATCH_SIZE + 1), height=six.text_type(HATCH_SIZE + 1), fill=fill, ) writer.element( "path", d=path_data, style=generate_css( { "fill": rgb2hex(stroke), "stroke": rgb2hex(stroke), "stroke-width": "1.0", "stroke-linecap": "butt", "stroke-linejoin": "miter", } ), ) writer.end("pattern") writer.end("defs") def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace """ attrib = {} forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: attrib["fill"] = "url(#%s)" % self._get_hatch(gc, rgbFace) if rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib["fill-opacity"] = str(rgbFace[3]) else: if rgbFace is None: attrib["fill"] = "none" else: if tuple(rgbFace[:3]) != (0, 0, 0): attrib["fill"] = rgb2hex(rgbFace) if len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib["fill-opacity"] = str(rgbFace[3]) if forced_alpha and gc.get_alpha() != 1.0: attrib["opacity"] = str(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib["stroke-dasharray"] = ",".join(["%f" % val for val in seq]) attrib["stroke-dashoffset"] = six.text_type(float(offset)) linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() attrib["stroke"] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: attrib["stroke-opacity"] = str(rgb[3]) if linewidth != 1.0: attrib["stroke-width"] = str(linewidth) if gc.get_joinstyle() != "round": attrib["stroke-linejoin"] = gc.get_joinstyle() if gc.get_capstyle() != "butt": attrib["stroke-linecap"] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id("p", dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start("defs") for clip, oid in six.itervalues(self._clipd): writer.start("clipPath", id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element("path", d=path_data) else: x, y, w, h = clip writer.element( "rect", x=six.text_type(x), y=six.text_type(y), width=six.text_type(w), height=six.text_type(h) ) writer.end("clipPath") writer.end("defs") def _write_svgfonts(self): if not rcParams["svg.fonttype"] == "svgfont": return writer = self.writer writer.start("defs") for font_fname, chars in six.iteritems(self._fonts): font = FT2Font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() writer.start("font", id=sfnt[(1, 0, 0, 4)]) writer.element( "font-face", attrib={ "font-family": font.family_name, "font-style": font.style_name.lower(), "units-per-em": "72", "bbox": " ".join(six.text_type(x / 64.0) for x in font.bbox), }, ) for char in chars: glyph = font.load_char(char, flags=LOAD_NO_HINTING) verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) # name = font.get_glyph_name(char) writer.element( "glyph", d=path_data, attrib={ # 'glyph-name': name, "unicode": unichr(char), "horiz-adv-x": six.text_type(glyph.linearHoriAdvance / 65536.0), }, ) writer.end("font") writer.end("defs") def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self.writer.start("g", id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start("g", id="%s_%d" % (s, self._groupd[s])) def close_group(self, s): self.writer.end("g") def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams["svg.image_noscale"] def _convert_path(self, path, transform=None, clip=None, simplify=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_svg(path, transform, clip, simplify, 6) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = rgbFace is None and gc.get_hatch_path() is None simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify) attrib = {} attrib["style"] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib["clip-path"] = "url(#%s)" % clipid if gc.get_url() is not None: self.writer.start("a", {"xlink:href": gc.get_url()}) self.writer.element("path", d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end("a") def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if not len(path.vertices): return writer = self.writer path_data = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) for key in list(six.iterkeys(style)): if not key.startswith("stroke"): del style[key] style = generate_css(style) if oid is None: oid = self._make_id("m", dictkey) writer.start("defs") writer.element("path", id=oid, d=path_data, style=style) writer.end("defs") self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib["clip-path"] = "url(#%s)" % clipid writer.start("g", attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {"xlink:href": "#%s" % oid} clip = (0, 0, self.width * 72, self.height * 72) for vertices, code in path.iter_segments(trans_and_flip, clip=clip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib["x"] = six.text_type(x) attrib["y"] = six.text_type(y) attrib["style"] = self._get_style(gc, rgbFace) writer.element("use", attrib=attrib) writer.end("g") def draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position, ): writer = self.writer path_codes = [] writer.start("defs") for i, (path, transform) in enumerate(self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = "C%x_%x_%s" % (self._path_collection_id, i, self._make_id("", d)) writer.element("path", id=oid, d=d) path_codes.append(oid) writer.end("defs") for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position, ): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start("a", attrib={"xlink:href": url}) if clipid is not None: writer.start("g", attrib={"clip-path": "url(#%s)" % clipid}) attrib = { "xlink:href": "#%s" % path_id, "x": six.text_type(xo), "y": six.text_type(self.height - yo), "style": self._get_style(gc0, rgbFace), } writer.element("use", attrib=attrib) if clipid is not None: writer.end("g") if url is not None: writer.end("a") self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start("filter", id="colorAdd") writer.element( "feComposite", attrib={"in": "SourceGraphic"}, in2="BackgroundImage", operator="arithmetic", k2="1", k3="1", ) writer.end("filter") avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start("defs") for i in range(3): x1, y1 = tpoints[i] x2, y2 = tpoints[(i + 1) % 3] x3, y3 = tpoints[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start( "linearGradient", id="GR%x_%d" % (self._n_gradients, i), x1=six.text_type(x1), y1=six.text_type(y1), x2=six.text_type(xb), y2=six.text_type(yb), ) writer.element( "stop", offset="0", style=generate_css({"stop-color": rgb2hex(c), "stop-opacity": six.text_type(c[-1])}) ) writer.element("stop", offset="1", style=generate_css({"stop-color": rgb2hex(c), "stop-opacity": "0"})) writer.end("linearGradient") writer.element( "polygon", id="GT%x" % self._n_gradients, points=" ".join([six.text_type(x) for x in (x1, y1, x2, y2, x3, y3)]), ) writer.end("defs") avg_color = np.sum(colors[:, :], axis=0) / 3.0 href = "#GT%x" % self._n_gradients writer.element( "use", attrib={"xlink:href": href, "fill": rgb2hex(avg_color), "fill-opacity": str(avg_color[-1])} ) for i in range(3): writer.element( "use", attrib={ "xlink:href": href, "fill": "url(#GR%x_%d)" % (self._n_gradients, i), "fill-opacity": "1", "filter": "url(#colorAdd)", }, ) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib["clip-path"] = "url(#%s)" % clipid self.writer.start("g", attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end("g") def option_scale_image(self): return True def get_image_magnification(self): return self.image_dpi / 72.0 def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start("g", attrib={"clip-path": "url(#%s)" % clipid}) trans = [1, 0, 0, 1, 0, 0] if rcParams["svg.image_noscale"]: trans = list(im.get_matrix()) trans[5] = -trans[5] attrib["transform"] = generate_transform([("matrix", tuple(trans))]) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() if dx is None: w = 72.0 * w / self.image_dpi else: w = dx if dy is None: h = 72.0 * h / self.image_dpi else: h = dy oid = getattr(im, "_gid", None) url = getattr(im, "_url", None) if url is not None: self.writer.start("a", attrib={"xlink:href": url}) if rcParams["svg.image_inline"]: bytesio = io.BytesIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, bytesio) im.flipud_out() oid = oid or self._make_id("image", bytesio) attrib["xlink:href"] = "data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode("ascii") else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = "%s.image%d.png" % (self.basename, self._imaged[self.basename]) verbose.report("Writing image file for inclusion: %s" % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() oid = oid or "Im_" + self._make_id("image", filename) attrib["xlink:href"] = filename alpha = gc.get_alpha() if alpha != 1.0: attrib["opacity"] = str(alpha) attrib["id"] = oid if transform is None: self.writer.element( "image", x=six.text_type(x / trans[0]), y=six.text_type((self.height - y) / trans[3] - h), width=six.text_type(w), height=six.text_type(h), attrib=attrib, ) else: flipped = self._make_flip_transform(transform) flipped = np.array(flipped.to_values()) y = y + dy if dy > 0.0: flipped[3] *= -1.0 y *= -1.0 attrib["transform"] = generate_transform([("matrix", flipped)]) self.writer.element( "image", x=six.text_type(x), y=six.text_type(y), width=six.text_type(dx), height=six.text_type(abs(dy)), attrib=attrib, ) if url is not None: self.writer.end("a") if clipid is not None: self.writer.end("g") def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != "#000000": style["fill"] = color if gc.get_alpha() != 1.0: style["opacity"] = six.text_type(gc.get_alpha()) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font(font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs if glyph_map_new: writer.start("defs") for char_id, glyph_path in six.iteritems(glyph_map_new): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element("path", id=char_id, d=path_data) writer.end("defs") glyph_map.update(glyph_map_new) attrib = {} attrib["style"] = generate_css(style) font_scale = fontsize / text2path.FONT_SCALE attrib["transform"] = generate_transform( [("translate", (x, y)), ("rotate", (-angle,)), ("scale", (font_scale, -font_scale))] ) writer.start("g", attrib=attrib) for glyph_id, xposition, yposition, scale in glyph_info: attrib = {"xlink:href": "#%s" % glyph_id} if xposition != 0.0: attrib["x"] = six.text_type(xposition) if yposition != 0.0: attrib["y"] = six.text_type(yposition) writer.element("use", attrib=attrib) writer.end("g") else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: writer.start("defs") for char_id, glyph_path in six.iteritems(glyph_map_new): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): path_data = "" else: path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element("path", id=char_id, d=path_data) writer.end("defs") glyph_map.update(glyph_map_new) attrib = {} font_scale = fontsize / text2path.FONT_SCALE attrib["style"] = generate_css(style) attrib["transform"] = generate_transform( [("translate", (x, y)), ("rotate", (-angle,)), ("scale", (font_scale, -font_scale))] ) writer.start("g", attrib=attrib) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element( "use", transform=generate_transform([("translate", (xposition, yposition)), ("scale", (scale,))]), attrib={"xlink:href": "#%s" % char_id}, ) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element("path", d=path_data) writer.end("g") def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != "#000000": style["fill"] = color if gc.get_alpha() != 1.0: style["opacity"] = six.text_type(gc.get_alpha()) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) fontsize = prop.get_size_in_points() fontfamily = font.family_name fontstyle = prop.get_style() attrib = {} # Must add "px" to workaround a Firefox bug style["font-size"] = six.text_type(fontsize) + "px" style["font-family"] = six.text_type(fontfamily) style["font-style"] = prop.get_style().lower() attrib["style"] = generate_css(style) if angle == 0 or mtext.get_rotation_mode() == "anchor": # If text anchoring can be supported, get the original # coordinates and add alignment information. # Get anchor coordinates. transform = mtext.get_transform() ax, ay = transform.transform_point(mtext.get_position()) ay = self.height - ay # Don't do vertical anchor alignment. Most applications do not # support 'alignment-baseline' yet. Apply the vertical layout # to the anchor point manually for now. angle_rad = angle * np.pi / 180.0 dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] ha_mpl_to_svg = {"left": "start", "right": "end", "center": "middle"} style["text-anchor"] = ha_mpl_to_svg[mtext.get_ha()] attrib["x"] = str(ax) attrib["y"] = str(ay) attrib["style"] = generate_css(style) attrib["transform"] = "rotate(%f, %f, %f)" % (-angle, ax, ay) writer.element("text", s, attrib=attrib) else: attrib["transform"] = generate_transform([("translate", (x, y)), ("rotate", (-angle,))]) writer.element("text", s, attrib=attrib) if rcParams["svg.fonttype"] == "svgfont": fontset = self._fonts.setdefault(font.fname, set()) for c in s: fontset.add(ord(c)) else: writer.comment(s) width, height, descent, svg_elements, used_characters = self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib["style"] = generate_css(style) attrib["transform"] = generate_transform([("translate", (x, y)), ("rotate", (-angle,))]) # Apply attributes to 'g', not 'text', because we likely # have some rectangles as well with the same style and # transformation writer.start("g", attrib=attrib) writer.start("text") # Sort the characters by font, and output one tspan for # each spans = {} for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css( { "font-size": six.text_type(fontsize) + "px", "font-family": font.family_name, "font-style": font.style_name.lower(), } ) if thetext == 32: thetext = 0xA0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) if rcParams["svg.fonttype"] == "svgfont": for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) for style, chars in list(six.iteritems(spans)): chars.sort() same_y = True if len(chars) > 1: last_y = chars[0][1] for i in xrange(1, len(chars)): if chars[i][1] != last_y: same_y = False break if same_y: ys = six.text_type(chars[0][1]) else: ys = " ".join(six.text_type(c[1]) for c in chars) attrib = {"style": style, "x": " ".join(six.text_type(c[0]) for c in chars), "y": ys} writer.element("tspan", "".join(unichr(c[2]) for c in chars), attrib=attrib) writer.end("text") if len(svg_rects): for x, y, width, height in svg_rects: writer.element( "rect", x=six.text_type(x), y=six.text_type(-y + height), width=six.text_type(width), height=six.text_type(height), ) writer.end("g") def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None): self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start("g", attrib={"clip-path": "url(#%s)" % clipid}) if gc.get_url() is not None: self.writer.start("a", {"xlink:href": gc.get_url()}) if rcParams["svg.fonttype"] == "path": self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: self.writer.end("a") if clipid is not None: self.writer.end("g") def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): return self._text2path.get_text_width_height_descent(s, prop, ismath)
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.dpi = dpi self.gc = GraphicsContextCairo (renderer=self) self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) self.mathtext_parser = MathTextParser('Cairo') def set_ctx_from_surface (self, surface): self.gc.ctx = cairo.Context (surface) def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) def _fill_and_stroke (self, ctx, fill_c, alpha): if fill_c is not None: ctx.save() if len(fill_c) == 3: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha*fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod def convert_path(ctx, path, transform): for points, code in path.iter_segments(transform): if code == Path.MOVETO: ctx.move_to(*points) elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: ctx.curve_to(points[0], points[1], points[0], points[1], points[2], points[3]) elif code == Path.CURVE4: ctx.curve_to(*points) elif code == Path.CLOSEPOLY: ctx.close_path() def draw_path(self, gc, path, transform, rgbFace=None): if len(path.vertices) > 18980: raise ValueError("The Cairo backend can not draw paths longer than 18980 points.") ctx = gc.ctx transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) ctx.new_path() self.convert_path(ctx, path, transform) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha()) def draw_image(self, gc, x, y, im): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) clippath, clippath_trans = gc.get_clip_path() im.flipud_out() rows, cols, buf = im.color_conv (BYTE_FORMAT) surface = cairo.ImageSurface.create_for_data ( buf, cairo.FORMAT_ARGB32, cols, rows, cols*4) ctx = self.gc.ctx ctx.save() if clippath is not None: ctx.new_path() RendererCairo.convert_path(ctx, clippath, clippath_trans) ctx.clip() y = self.height - y - rows ctx.set_source_surface (surface, x, y) ctx.paint() ctx.restore() im.flipud_out() def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate (-angle * npy.pi / 180) ctx.set_font_size (size) ctx.show_text (s.encode("utf-8")) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate (-angle * npy.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) ctx.show_text(s.encode("utf-8")) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle (ox, oy, w, h) ctx.set_source_rgb (0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return True def get_canvas_width_height(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.set_font_size (size) y_bearing, w, h = ctx.text_extents (s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.gc.ctx.save() return self.gc def points_to_pixels(self, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return points/72.0 * self.dpi
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._n_gradients = 0 self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog % (width, height, width, height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid if gc.get_url() is not None: self._svgwriter.write('<a xlink:href="%s">' % gc.get_url()) style = self._get_style(gc, rgbFace) self._svgwriter.write('<%s style="%s" %s %s/>\n' % (element, style, clippath, details)) if gc.get_url() is not None: self._svgwriter.write('</a>') def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ HATCH_SIZE = 72 dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) id = self._hatchd.get(dictkey) if id is None: id = 'h%s' % md5(str(dictkey)).hexdigest() self._svgwriter.write('<defs>\n <pattern id="%s" ' % id) self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ') self._svgwriter.write(' width="%d" height="%d" >\n' % (HATCH_SIZE, HATCH_SIZE)) path_data = self._convert_path(gc.get_hatch_path(), Affine2D().scale(HATCH_SIZE).scale( 1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) self._svgwriter.write( '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (HATCH_SIZE + 1, HATCH_SIZE + 1, fill)) path = '<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % ( path_data, rgb2hex(gc.get_rgb()), rgb2hex(gc.get_rgb())) self._svgwriter.write(path) self._svgwriter.write('\n </pattern>\n</defs>') self._hatchd[dictkey] = id return id def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if gc.get_hatch() is not None: fill = "url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % ( ','.join(['%f' % val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %f; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % ( fill, rgb2hex(gc.get_rgb()), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %f' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) path_data = self._convert_path(clippath, clippath_trans, simplify=False) path = '<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) path = '<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals( ) else: return None id = self._clipd.get(path) if id is None: id = 'p%s' % md5(path).hexdigest() self._svgwriter.write('<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write('\n </clipPath>\n</defs>') self._clipd[path] = id return id def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self._svgwriter.write('<g id="%s">\n' % (gid)) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] _path_commands = { Path.MOVETO: 'M%f %f', Path.LINETO: 'L%f %f', Path.CURVE3: 'Q%f %f %f %f', Path.CURVE4: 'C%f %f %f %f %f %f' } def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _convert_path(self, path, transform, clip=False, simplify=None): path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None for points, code in path.iter_segments(transform, clip=clip, simplify=simplify): if code == Path.CLOSEPOLY: segment = 'z' else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender("\n") currpos = 0 appender(segment) currpos += len(segment) return ''.join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify) self._draw_svg_element('path', 'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) name = self._markers.get(key) if name is None: name = 'm%s' % md5(key).hexdigest() write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>' % clippath) trans_and_flip = self._make_flip_transform(trans) for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) write('</g>') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._svgwriter.write path_codes = [] write('<defs>\n') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) name = 'coll%x_%x_%s' % (self._path_collection_id, i, md5(d).hexdigest()) write('<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write('</defs>\n') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): clipid = self._get_gc_clip_svg(gc0) url = gc0.get_url() if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) if clipid is not None: write('<g clip-path="url(#%s)">' % clipid) details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc0, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write('</g>') if url is not None: self._svgwriter.write('</a>') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) write = self._svgwriter.write write('<defs>') for i in range(3): x1, y1 = points[i] x2, y2 = points[(i + 1) % 3] x3, y3 = points[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 write( '<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">' % (self._n_gradients, i, x1, y1, xb, yb)) write('<stop offset="0" style="stop-color:%s;stop-opacity:%f"/>' % (rgb2hex(c), c[-1])) write('<stop offset="1" style="stop-color:%s;stop-opacity:0"/>' % rgb2hex(c)) write('</linearGradient>') # Define the triangle itself as a "def" since we use it 4 times write('<polygon id="GT%x" points="%f %f %f %f %f %f"/>' % (self._n_gradients, x1, y1, x2, y2, x3, y3)) write('</defs>\n') avg_color = np.sum(colors[:, :], axis=0) / 3.0 write('<use xlink:href="#GT%x" fill="%s" fill-opacity="%f"/>\n' % (self._n_gradients, rgb2hex(avg_color), avg_color[-1])) for i in range(3): write( '<use xlink:href="#GT%x" fill="url(#GR%x_%d)" fill-opacity="1" filter="url(#colorAdd)"/>\n' % (self._n_gradients, self._n_gradients, i)) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): write = self._svgwriter.write clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>\n' % clippath) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) write('</g>\n') def draw_image(self, gc, x, y, im): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid trans = [1, 0, 0, 1, 0, 0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] transstr = 'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() url = getattr(im, '_url', None) if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) self._svgwriter.write( '<image x="%f" y="%f" width="%f" height="%f" ' '%s %s xlink:href="' % (x / trans[0], (self.height - y) / trans[3] - h, w, h, transstr, clippath)) if rcParams['svg.image_inline']: self._svgwriter.write("data:image/png;base64,\n") stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() self._svgwriter.write(base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write('"/>\n') if url is not None: self._svgwriter.write('</a>') def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ # this method works for normal text, mathtext and usetex mode. # But currently only utilized by draw_tex method. glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() write = self._svgwriter.write if ismath == False: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs _flip = Affine2D().scale(1.0, -1.0) if glyph_map_new: write('<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): path = Path(*glyph_path) path_data = self._convert_path(path, _flip, simplify=False) path_element = '<path id="%s" d="%s"/>\n' % ( char_id, ''.join(path_data)) write(path_element) write('</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / text2path.FONT_SCALE)) for glyph_id, xposition, yposition, scale in glyph_info: svg.append('<use xlink:href="#%s"' % glyph_id) svg.append(' x="%f" y="%f"' % (xposition, yposition)) #(currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map) else: _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: write('<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): char_id = self._adjust_char_id(char_id) path = Path(*glyph_path) path_data = self._convert_path(path, None, simplify=False) #_flip) path_element = '<path id="%s" d="%s"/>\n' % ( char_id, ''.join(path_data)) write(path_element) write('</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f,-%f)">\n' % (fontsize / text2path.FONT_SCALE, fontsize / text2path.FONT_SCALE)) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) svg.append('<use xlink:href="#%s"' % char_id) svg.append(' x="%f" y="%f" transform="scale(%f)"' % (xposition / scale, yposition / scale, scale)) svg.append('/>\n') for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, None, simplify=False) path_element = '<path d="%s"/>\n' % (''.join(path_data)) svg.append(path_element) svg.append('</g><!-- style -->\n') if clipid is not None: svg.append('</g><!-- clipid -->\n') svg = ''.join(svg) write(svg) def draw_tex(self, gc, x, y, s, prop, angle): self.draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' x="%f"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ( 'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f' % (fontsize, fontfamily, fontstyle, color, gc.get_alpha())) if angle != 0: transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % ( x, y, -angle, -x, -y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append( "c%f %f %f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = ''.join(path_data) char_num = 'c_%s' % md5(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write style = "fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append( '<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append( ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y)) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x, curr_y = 0.0, 0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = -new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%f,%f) rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append( '<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self.open_group("mathtext") write(''.join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: size = prop.get_size_in_points() texmanager = self._text2path.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class RendererH5Canvas(RendererBase): """The renderer handles drawing/rendering operations.""" fontd = maxdict(50) def __init__(self, width, height, ctx, dpi=72): self.width = width self.height = height self.dpi = dpi self.ctx = ctx self._image_count = 0 # used to uniquely label each image created in this figure... # define the js context self.ctx.width = width self.ctx.height = height #self.ctx.textAlign = "center"; self.ctx.textBaseline = "alphabetic" self.flip = Affine2D().scale(1, -1).translate(0, height) self.mathtext_parser = MathTextParser('bitmap') self._path_time = 0 self._text_time = 0 self._marker_time = 0 self._sub_time = 0 self._last_clip = None self._last_clip_path = None self._clip_count = 0 def _set_style(self, gc, rgbFace=None): ctx = self.ctx if rgbFace is not None: ctx.fillStyle = mpl_to_css_color(rgbFace, gc.get_alpha()) ctx.strokeStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) if gc.get_capstyle(): ctx.lineCap = _capstyle_d[gc.get_capstyle()] ctx.lineWidth = self.points_to_pixels(gc.get_linewidth()) def _path_to_h5(self, ctx, path, transform, clip=None, stroke=True, dashes=(None, None)): """Iterate over a path and produce h5 drawing directives.""" transform = transform + self.flip ctx.beginPath() current_point = None dash_offset, dash_pattern = dashes if dash_pattern is not None: dash_offset = self.points_to_pixels(dash_offset) dash_pattern = tuple([self.points_to_pixels(dash) for dash in dash_pattern]) for points, code in path.iter_segments(transform, clip=clip): # Shift all points by half a pixel, so that integer coordinates are aligned with pixel centers instead of edges # This prevents lines that are one pixel wide and aligned with the pixel grid from being rendered as a two-pixel wide line # This happens because HTML Canvas defines (0, 0) as the *top left* of a pixel instead of the center, # which causes all integer-valued coordinates to fall exactly between pixels points += 0.5 if code == Path.MOVETO: ctx.moveTo(points[0], points[1]) current_point = (points[0], points[1]) elif code == Path.LINETO: t = time.time() if (dash_pattern is None) or (current_point is None): ctx.lineTo(points[0], points[1]) else: dash_offset = ctx.dashedLine(current_point[0], current_point[1], points[0], points[1], (dash_offset, dash_pattern)) self._sub_time += time.time() - t current_point = (points[0], points[1]) elif code == Path.CURVE3: ctx.quadraticCurveTo(*points) current_point = (points[2], points[3]) elif code == Path.CURVE4: ctx.bezierCurveTo(*points) current_point = (points[4], points[5]) else: pass if stroke: ctx.stroke() def _do_path_clip(self, ctx, clip): self._clip_count += 1 ctx.save() ctx.beginPath() ctx.moveTo(clip[0],clip[1]) ctx.lineTo(clip[2],clip[1]) ctx.lineTo(clip[2],clip[3]) ctx.lineTo(clip[0],clip[3]) ctx.clip() def draw_path(self, gc, path, transform, rgbFace=None): t = time.time() self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) clippath, cliptrans = gc.get_clip_path() ctx = self.ctx if clippath is not None and self._last_clip_path != clippath: ctx.restore() ctx.save() self._path_to_h5(ctx, clippath, cliptrans, None, stroke=False) ctx.clip() self._last_clip_path = clippath if self._last_clip != clip and clip is not None and clippath is None: ctx.restore() self._do_path_clip(ctx, clip) self._last_clip = clip if clip is None and clippath is None and (self._last_clip is not None or self._last_clip_path is not None): self._reset_clip() if rgbFace is None and gc.get_hatch() is None: figure_clip = (0, 0, self.width, self.height) else: figure_clip = None self._path_to_h5(ctx, path, transform, figure_clip, dashes=gc.get_dashes()) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._path_time += time.time() - t def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() if cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) return (x,y,x+w,y+h) return None def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): t = time.time() for vertices, codes in path.iter_segments(trans, simplify=False): if len(vertices): x,y = vertices[-2:] self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) ctx = self.ctx self._path_to_h5(ctx, marker_path, marker_trans + Affine2D().translate(x, y), clip) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._marker_time += time.time() - t def _slipstream_png(self, x, y, im_buffer, width, height): """Insert image directly into HTML canvas as base64-encoded PNG.""" # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) # Write the image into a WebPNG object f = WebPNG() _png.write_png(im_buffer, width, height, f) # Write test PNG as file as well #_png.write_png(im_buffer, width, height, 'canvas_image_%d.png' % (self._image_count,)) # Extract the base64-encoded PNG and send it to the canvas uname = str(uuid.uuid1()).replace("-","") #self.ctx._context_name + str(self._image_count) # try to use a unique image name enc = "var canvas_image_%s = 'data:image/png;base64,%s';" % (uname, f.get_b64()) s = "function imageLoaded_%s(ev) {\nim = ev.target;\nim_left_to_load_%s -=1;\nif (im_left_to_load_%s == 0) frame_body_%s();\n}\ncanv_im_%s = new Image();\ncanv_im_%s.onload = imageLoaded_%s;\ncanv_im_%s.src = canvas_image_%s;\n" % \ (uname, self.ctx._context_name, self.ctx._context_name, self.ctx._context_name, uname, uname, uname, uname, uname) self.ctx.add_header(enc) self.ctx.add_header(s) # Once the base64 encoded image has been received, draw it into the canvas self.ctx.write("%s.drawImage(canv_im_%s, %g, %g, %g, %g);" % (self.ctx._context_name, uname, x, y, width, height)) # draw the image as loaded into canv_im_%d... self._image_count += 1 def _reset_clip(self): self.ctx.restore() self._last_clip = None self._last_clip_path = None #<1.0.0: def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): #1.0.0 and up: def draw_image(self, gc, x, y, im, clippath=None): #API for draw image changed between 0.99 and 1.0.0 def draw_image(self, *args, **kwargs): x, y, im = args[:3] try: h,w = im.get_size_out() except AttributeError: x, y, im = args[1:4] h,w = im.get_size_out() clippath = (kwargs.has_key('clippath') and kwargs['clippath'] or None) if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() if clippath is not None: self._path_to_h5(self.ctx,clippath, clippath_trans, stroke=False) self.ctx.save() self.ctx.clip() (x,y) = self.flip.transform((x,y)) im.flipud_out() rows, cols, im_buffer = im.as_rgba_str() self._slipstream_png(x, (y-h), im_buffer, cols, rows) if clippath is not None: self.ctx.restore() def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() font.set_size(prop.get_size_in_points(), self.dpi) return font def draw_tex(self, gc, x, y, s, prop, angle, ismath=False): logger.error("Tex support is currently not implemented. Text element '%s' will not be displayed..." % s) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() t = time.time() if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return angle = math.radians(angle) width, height, descent = self.get_text_width_height_descent(s, prop, ismath) x -= math.sin(angle) * descent y -= math.cos(angle) * descent ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) font_size = self.points_to_pixels(prop.get_size_in_points()) font_str = '%s %s %.3gpx %s, %s' % (prop.get_style(), prop.get_weight(), font_size, prop.get_name(), prop.get_family()[0]) ctx.font = font_str # Set the text color, draw the text and reset the color to black afterwards ctx.fillStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) ctx.fillText(unicode(s), x, y) ctx.fillStyle = '#000000' if angle != 0: ctx.restore() self._text_time = time.time() - t def _draw_mathtext(self, gc, x, y, s, prop, angle): """Draw math text using matplotlib.mathtext.""" # Render math string as an image at the configured DPI, and get the image dimensions and baseline depth rgba, descent = self.mathtext_parser.to_rgba(s, color=gc.get_rgb(), dpi=self.dpi, fontsize=prop.get_size_in_points()) height, width, tmp = rgba.shape angle = math.radians(angle) # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) # Insert math text image into stream, and adjust x, y reference point to be at top left of image self._slipstream_png(x, y - height, rgba.tostring(), width, height) if angle != 0: ctx.restore() def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: image, d = self.mathtext_parser.parse(s, self.dpi, prop) w, h = image.get_width(), image.get_height() else: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() / 64.0 return w, h, d def new_gc(self): return GraphicsContextH5Canvas() def points_to_pixels(self, points): # The standard desktop-publishing (Postscript) point is 1/72 of an inch return points/72.0 * self.dpi
class TextToPath(object): """ A class that convert a given text to a path using ttf fonts. """ FONT_SCALE = 100. DPI = 72 def __init__(self): """ Initialization """ self.mathtext_parser = MathTextParser('path') self.tex_font_map = None from matplotlib.cbook import maxdict self._ps_fontd = maxdict(50) self._texmanager = None self._adobe_standard_encoding = None def _get_adobe_standard_encoding(self): enc_name = dviread.find_tex_file('8a.enc') enc = dviread.Encoding(enc_name) return dict([(c, i) for i, c in enumerate(enc.encoding)]) def _get_font(self, prop): """ find a ttf font. """ fname = font_manager.findfont(prop) font = FT2Font(str(fname)) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%x' % (ps_name, ccode)) return char_id def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.quote('%s-%d' % (ps_name, ccode)) return char_id def glyph_to_path(self, font, currx=0.): """ convert the ft2font glyph to vertices and codes. """ verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = float(fontsize) / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False, usetex=False): """ convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. Effective only if usetex == False. """ if not usetex: if not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font( font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext( prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. cmap = font.get_charmap() lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = (glyph.linearHoriAdvance / 65536.0) char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: glyph_map_new[char_id] = self.glyph_to_path(font) currx += (kern / 64.0) xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes by parsing it with mathtext. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] currx, curry = 0, 0 for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = self.glyph_to_path(font) xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, myrects) def get_texmanager(self): """ return the :class:`matplotlib.texmanager.TexManager` instance """ if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using matplotlib's usetex mode. """ # codes are modstly borrowed from pdf backend. texmanager = self.get_texmanager() if self.tex_font_map is None: self.tex_font_map = dviread.PsfontsMap( dviread.find_tex_file('pdftex.map')) if self._adobe_standard_encoding is None: self._adobe_standard_encoding = self._get_adobe_standard_encoding() fontsize = prop.get_size_in_points() if hasattr(texmanager, "get_dvi"): dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE) dvi = dviread.DviFromFileLike(dvifilelike, self.DPI) else: dvifile = texmanager.make_dvi(s, self.FONT_SCALE) dvi = dviread.Dvi(dvifile, self.DPI) try: page = next(iter(dvi)) finally: dvi.close() if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. # oldfont, seq = None, [] for x1, y1, dvifont, glyph, width in page.text: font_and_encoding = self._ps_fontd.get(dvifont.texname) font_bunch = self.tex_font_map[dvifont.texname] if font_and_encoding is None: font = FT2Font(str(font_bunch.filename)) for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451), ("ADOBE_STANDARD", 1094995778)]: try: font.select_charmap(charmap_code) except ValueError: pass else: break else: charmap_name = "" warnings.warn("No supported encoding in font (%s)." % font_bunch.filename) if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding: enc0 = dviread.Encoding(font_bunch.encoding) enc = dict([(i, self._adobe_standard_encoding.get(c, None)) for i, c in enumerate(enc0.encoding)]) else: enc = dict() self._ps_fontd[dvifont.texname] = font, enc else: font, enc = font_and_encoding ft2font_flag = LOAD_TARGET_LIGHT char_id = self._get_char_id_ps(font, glyph) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) if enc: charcode = enc.get(glyph, None) else: charcode = glyph if charcode is not None: glyph0 = font.load_char(charcode, flags=ft2font_flag) else: warnings.warn("The glyph (%d) of font (%s) cannot be " "converted with the encoding. Glyph may " "be wrong" % (glyph, font_bunch.filename)) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = self.glyph_to_path(font) glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, myrects)
class TextToPath(object): """A class that converts strings to paths.""" FONT_SCALE = 100. DPI = 72 def __init__(self): self.mathtext_parser = MathTextParser('path') self._texmanager = None @property @cbook.deprecated("3.0") def tex_font_map(self): return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) def _get_font(self, prop): """ Find the `FT2Font` matching font properties *prop*, with its size set. """ fname = font_manager.findfont(prop) font = get_font(fname) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode)) def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode)) return char_id @cbook.deprecated( "3.1", alternative="font.get_path() and manual translation of the vertices") def glyph_to_path(self, font, currx=0.): """Convert the *font*'s current glyph to a (vertices, codes) pair.""" verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = fontsize / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False, usetex=False): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). Parameters ---------- prop : `matplotlib.font_manager.FontProperties` instance The font properties for the text. s : str The text to be converted. usetex : bool, optional Whether to use tex rendering. Defaults to ``False``. ismath : bool, optional If True, use mathtext parser. Effective only if ``usetex == False``. Returns ------- verts, codes : tuple of lists *verts* is a list of numpy arrays containing the x and y coordinates of the vertices. *codes* is a list of path codes. Examples -------- Create a list of vertices and codes from a text, and create a `Path` from those:: from matplotlib.path import Path from matplotlib.textpath import TextToPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Humor Sans", style="italic") verts, codes = TextToPath().get_text_path(fp, "ABC") path = Path(verts, codes, closed=False) Also see `TextPath` for a more direct way to create a path from a text. """ if not usetex: if not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font( font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext( prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ Convert string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = glyph.linearHoriAdvance / 65536 char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: glyph_map_new[char_id] = font.get_path() currx += kern / 64 xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Parse mathtext string *s* and convert it to a (vertices, codes) pair. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Process string *s* with usetex and convert it to a (vertices, codes) pair. """ # Implementation mostly borrowed from pdf backend. dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) with dviread.Dvi(dvifile, self.DPI) as dvi: page, = dvi if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. for x1, y1, dvifont, glyph, width in page.text: font, enc = self._get_ps_font_and_encoding(dvifont.texname) char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) if enc: charcode = enc.get(glyph, None) else: charcode = glyph ft2font_flag = LOAD_TARGET_LIGHT if charcode is not None: glyph0 = font.load_char(charcode, flags=ft2font_flag) else: _log.warning("The glyph (%d) of font (%s) cannot be " "converted with the encoding. Glyph may " "be wrong.", glyph, font.fname) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) font_bunch = tex_font_map[texname] if font_bunch.filename is None: raise ValueError( ("No usable font file found for %s (%s). " "The font may lack a Type-1 version.") % (font_bunch.psname, texname)) font = get_font(font_bunch.filename) for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451), ("ADOBE_STANDARD", 1094995778)]: try: font.select_charmap(charmap_code) except (ValueError, RuntimeError): pass else: break else: charmap_name = "" _log.warning("No supported encoding in font (%s).", font_bunch.filename) if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding: enc0 = dviread.Encoding(font_bunch.encoding) enc = {i: _get_adobe_standard_encoding().get(c, None) for i, c in enumerate(enc0.encoding)} else: enc = {} return font, enc
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug = 1 def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report( 'RendererAgg.__init__ width=%s, height=%s' % (width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def _get_hinting_flag(self): if rcParams['text.hinting']: return LOAD_FORCE_AUTOHINT else: return LOAD_NO_HINTING def draw_markers(self, *kl, **kw): # for filtering to work with rastrization, methods needs to be wrapped. # maybe there is better way to do it. return self._renderer.draw_markers(*kl, **kw) def _update_methods(self): #self.draw_path = self._renderer.draw_path # see below #self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = np.ceil(npts / float(nmax)) chsize = int(np.ceil(npts / nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1, :] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = self._get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=flags) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap() #print x, y, int(x), int(y), s self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make cacheing in the # texmanager more efficient. It is not meant to be used # outside the backend """ if rcParams['text.usetex']: # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = self._get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points * self.dpi / 72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self, x, y): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba(x, y) def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): """ agg backend support arbitrary scaling of image. """ return True def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy self._renderer.restore_region2(region, x1, y1, x2, y2, ox, oy) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ from matplotlib._image import fromarray width, height = int(self.width), int(self.height) buffer, bounds = self._renderer.tostring_rgba_minimized() l, b, w, h = bounds self._renderer = self._filter_renderers.pop() self._update_methods() if w > 0 and h > 0: img = np.fromstring(buffer, np.uint8) img, ox, oy = post_processing( img.reshape((h, w, 4)) / 255., self.dpi) image = fromarray(img, 1) image.flipud_out() gc = self.new_gc() self._renderer.draw_image(gc, l + ox, height - b - h + oy, image)
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])
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug=1 def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = npy.ceil(npts/float(nmax)) chsize = int(npy.ceil(npts/nch)) i0 = npy.arange(0, npts, chsize) i1 = npy.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1,:] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT) else: font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT) font.draw_glyphs_to_bitmap() self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop """ if ismath=='TeX': size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = npy.array(Z * 255.0, npy.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points*self.dpi/72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self,x,y): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba(x,y) def clear(self): self._renderer.clear() def option_image_nocomposite(self): return True def restore_region(self, region, bbox=None, xy=None): """ restore the saved region. if bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specify the new position (of the LLC of the originally region, not the LLC of the bbox) that the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy self._renderer.restore_region2(region, x1, y1, x2, y2, ox, oy) else: self._renderer.restore_region(region)
class RendererCairo(RendererBase): @cbook.deprecated("3.3") @property def fontweights(self): return { 100: cairo.FONT_WEIGHT_NORMAL, 200: cairo.FONT_WEIGHT_NORMAL, 300: cairo.FONT_WEIGHT_NORMAL, 400: cairo.FONT_WEIGHT_NORMAL, 500: cairo.FONT_WEIGHT_NORMAL, 600: cairo.FONT_WEIGHT_BOLD, 700: cairo.FONT_WEIGHT_BOLD, 800: cairo.FONT_WEIGHT_BOLD, 900: cairo.FONT_WEIGHT_BOLD, 'ultralight': cairo.FONT_WEIGHT_NORMAL, 'light': cairo.FONT_WEIGHT_NORMAL, 'normal': cairo.FONT_WEIGHT_NORMAL, 'medium': cairo.FONT_WEIGHT_NORMAL, 'regular': cairo.FONT_WEIGHT_NORMAL, 'semibold': cairo.FONT_WEIGHT_BOLD, 'bold': cairo.FONT_WEIGHT_BOLD, 'heavy': cairo.FONT_WEIGHT_BOLD, 'ultrabold': cairo.FONT_WEIGHT_BOLD, 'black': cairo.FONT_WEIGHT_BOLD, } @cbook.deprecated("3.3") @property def fontangles(self): return { 'italic': cairo.FONT_SLANT_ITALIC, 'normal': cairo.FONT_SLANT_NORMAL, 'oblique': cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): self.dpi = dpi self.gc = GraphicsContextCairo(renderer=self) self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) # Although it may appear natural to automatically call # `self.set_width_height(surface.get_width(), surface.get_height())` # here (instead of having the caller do so separately), this would fail # for PDF/PS/SVG surfaces, which have no way to report their extents. def set_width_height(self, width, height): self.width = width self.height = height def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: ctx.save() if len(fill_c) == 3 or alpha_overrides: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited ctx = gc.ctx # Clip the path to the actual rendering extents if it isn't filled. clip = (ctx.clip_extents() if rgbFace is None and gc.get_hatch() is None else None) transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() _append_path(ctx, path, transform, clip) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None): # docstring inherited ctx = gc.ctx ctx.new_path() # Create the path for the marker; it needs to be flipped here already! _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill x1, y1, x2, y2 = ctx.fill_extents() if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0: filled = False # No fill, just unset this (so we don't try to fill it later on) rgbFace = None else: filled = True transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() for i, (vertices, codes) in enumerate( path.iter_segments(transform, simplify=False)): if len(vertices): x, y = vertices[-2:] ctx.save() # Translate and apply path ctx.translate(x, y) ctx.append_path(marker_path) ctx.restore() # Slower code path if there is a fill; we need to draw # the fill and stroke for each marker at the same time. # Also flush out the drawing every once in a while to # prevent the paths from getting way too long. if filled or i % 1000 == 0: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) # Fast path, if there is no fill, draw everything in one step if not filled: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_image(self, gc, x, y, im): im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1]) surface = cairo.ImageSurface.create_for_data(im.ravel().data, cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1] * 4) ctx = gc.ctx y = self.height - y - im.shape[0] ctx.save() ctx.set_source_surface(surface, float(x), float(y)) ctx.paint() ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited # Note: (x, y) are device/display coords, not user-coords, unlike other # draw_* methods if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to(x, y) ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) ctx.save() ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) if angle: ctx.rotate(np.deg2rad(-angle)) ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate(np.deg2rad(-angle)) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) ctx.select_font_face( *_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(fontsize * self.dpi / 72) ctx.show_text(s) for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle(ox, oy, w, h) ctx.set_source_rgb(0, 0, 0) ctx.fill_preserve() ctx.restore() def get_canvas_width_height(self): # docstring inherited return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath == 'TeX': return super().get_text_width_height_descent(s, prop, ismath) if ismath: dims = self.mathtext_parser.parse(s, self.dpi, prop) return dims[0:3] # return width, height, descent ctx = self.text_ctx # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.save() ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): # docstring inherited self.gc.ctx.save() self.gc._alpha = 1 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): # docstring inherited return points / 72 * self.dpi
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.dpi = dpi self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) self.mathtext_parser = MathTextParser('Cairo') def set_ctx_from_surface (self, surface): self.ctx = cairo.Context (surface) self.ctx.save() # restore, save - when call new_gc() def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke (self, ctx, fill_c, alpha): if fill_c is not None: ctx.save() if len(fill_c) == 3: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha*fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() #@staticmethod def convert_path(ctx, tpath): for points, code in tpath.iter_segments(): if code == Path.MOVETO: ctx.move_to(*points) elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: ctx.curve_to(points[0], points[1], points[0], points[1], points[2], points[3]) elif code == Path.CURVE4: ctx.curve_to(*points) elif code == Path.CLOSEPOLY: ctx.close_path() convert_path = staticmethod(convert_path) def draw_path(self, gc, path, transform, rgbFace=None): if len(path.vertices) > 18980: raise ValueError("The Cairo backend can not draw paths longer than 18980 points.") ctx = gc.ctx transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) tpath = transform.transform_path(path) ctx.new_path() self.convert_path(ctx, tpath) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha()) def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): # bbox - not currently used if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) im.flipud_out() rows, cols, buf = im.color_conv (BYTE_FORMAT) surface = cairo.ImageSurface.create_for_data ( buf, cairo.FORMAT_ARGB32, cols, rows, cols*4) # function does not pass a 'gc' so use renderer.ctx ctx = self.ctx y = self.height - y - rows ctx.set_source_surface (surface, x, y) ctx.paint() im.flipud_out() def draw_text(self, gc, x, y, s, prop, angle, ismath=False): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate (-angle * npy.pi / 180) ctx.set_font_size (size) ctx.show_text (s.encode("utf-8")) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate (-angle * npy.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) ctx.show_text(s.encode("utf-8")) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle (ox, oy, w, h) ctx.set_source_rgb (0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size (size) y_bearing, w, h = ctx.text_extents (s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.ctx.restore() # matches save() in set_ctx_from_surface() self.ctx.save() return GraphicsContextCairo (renderer=self) def points_to_pixels(self, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return points/72.0 * self.dpi
class RendererGR(RendererBase): """ Handles drawing/rendering operations using GR """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi): self.dpi = dpi self.width = 640.0 * dpi / 80 self.height = 480.0 * dpi / 80 mwidth, mheight, width, height = gr.inqdspsize() if (width / (mwidth / 0.0256) < 200): mwidth *= self.width / width gr.setwsviewport(0, mwidth, 0, mwidth * 0.75) else: gr.setwsviewport(0, 0.192, 0, 0.144) gr.setwswindow(0, 1, 0, 0.75) gr.setviewport(0, 1, 0, 0.75) gr.setwindow(0, self.width, 0, self.height) self.mathtext_parser = MathTextParser('agg') self.texmanager = TexManager() def draw_path(self, gc, path, transform, rgbFace=None): path = transform.transform_path(path) points = path.vertices codes = path.codes bbox = gc.get_clip_rectangle() if bbox is not None: x, y, w, h = bbox.bounds clrt = np.array([x, x + w, y, y + h]) else: clrt = np.array([0, self.width, 0, self.height]) gr.setviewport(*clrt/self.width) gr.setwindow(*clrt) if rgbFace is not None and len(points) > 2: color = gr.inqcolorfromrgb(rgbFace[0], rgbFace[1], rgbFace[2]) gr.settransparency(rgbFace[3]) gr.setcolorrep(color, rgbFace[0], rgbFace[1], rgbFace[2]) gr.setfillintstyle(gr.INTSTYLE_SOLID) gr.setfillcolorind(color) gr.drawpath(points, codes, fill=True) lw = gc.get_linewidth() if lw != 0: rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) if type(gc._linestyle) is unicode: gr.setlinetype(linetype[gc._linestyle]) gr.setlinewidth(lw) gr.setlinecolorind(color) gr.drawpath(points, codes, fill=False) def draw_image(self, gc, x, y, im): h, w, s = im.as_rgba_str() img = np.fromstring(s, np.uint32) img.shape = (h, w) gr.drawimage(x, x + w, y + h, y, w, h, img) def draw_mathtext(self, x, y, angle, Z): h, w = Z.shape img = np.zeros((h, w), np.uint32) for i in range(h): for j in range(w): img[i, j] = (255 - Z[i, j]) << 24 a = int(angle) if a == 90: gr.drawimage(x - h, x, y, y + w, h, w, np.resize(np.rot90(img, 1), (h, w))) elif a == 180: gr.drawimage(x - w, x, y - h, y, w, h, np.rot90(img, 2)) elif a == 270: gr.drawimage(x, x + h, y - w, y, h, w, np.resize(np.rot90(img, 3), (h, w))) else: gr.drawimage(x, x + w, y, y + h, w, h, img) def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): size = prop.get_size_in_points() key = s, size, self.dpi, angle, self.texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = self.texmanager.get_grey(s, size, self.dpi) Z = np.array(255.0 - Z * 255.0, np.uint8) self.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) self.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: x, y = gr.wctondc(x, y) s = s.replace(u'\u2212', '-') fontsize = prop.get_size_in_points() rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) gr.setcharheight(fontsize * 0.0013) gr.settextcolorind(color) if angle != 0: gr.setcharup(-np.sin(angle * np.pi/180), np.cos(angle * np.pi/180)) else: gr.setcharup(0, 1) gr.text(x, y, s.encode("latin-1")) def flipy(self): return False def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': fontsize = prop.get_size_in_points() w, h, d = self.texmanager.get_text_width_height_descent( s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent # family = prop.get_family() # weight = prop.get_weight() # style = prop.get_style() s = s.replace(u'\u2212', '-').encode("latin-1") fontsize = prop.get_size_in_points() gr.setcharheight(fontsize * 0.0013) gr.setcharup(0, 1) (tbx, tby) = gr.inqtextext(0, 0, s) width, height, descent = tbx[1], tby[2], 0 return width, height, descent def new_gc(self): return GraphicsContextGR() def points_to_pixels(self, points): return points
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug=1 texd = maxdict(50) # a cache of tex image rasters _fontd = maxdict(50) def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') #self.draw_path = self._renderer.draw_path # see below self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.restore_region = self._renderer.restore_region self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def draw_path(self, gc, path, transform, rgbFace=None): nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None: nch = npy.ceil(npts/float(nmax)) chsize = int(npy.ceil(npts/nch)) i0 = npy.arange(0, npts, chsize) i1 = npy.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1,:] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT) font.draw_glyphs_to_bitmap() #print x, y, int(x), int(y) self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make cacheing in the # texmanager more efficient. It is not meant to be used # outside the backend """ if ismath=='TeX': # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) m,n = Z.shape # TODO: descent of TeX text (I am imitating backend_ps here -JKS) return n, m, 0 if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = npy.array(Z * 255.0, npy.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points*self.dpi/72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self,x,y): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba(x,y) def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True
class RendererGR(RendererBase): """ Handles drawing/rendering operations using GR """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): self.dpi = dpi if __version__[0] >= '2': self.nominal_fontsize = 0.001625 default_dpi = 100 else: self.nominal_fontsize = 0.0013 default_dpi = 80 self.width = float(width) * dpi / default_dpi self.height = float(height) * dpi / default_dpi self.mathtext_parser = MathTextParser('agg') self.texmanager = TexManager() def configure(self): aspect_ratio = self.width / self.height if aspect_ratio > 1: rect = np.array([0, 1, 0, 1.0 / aspect_ratio]) self.size = self.width else: rect = np.array([0, aspect_ratio, 0, 1]) self.size = self.height mwidth, mheight, width, height = gr.inqdspsize() if width / (mwidth / 0.0256) < 200: mwidth *= self.width / width gr.setwsviewport(*rect * mwidth) else: gr.setwsviewport(*rect * 0.192) gr.setwswindow(*rect) gr.setviewport(*rect) gr.setwindow(0, self.width, 0, self.height) def draw_path(self, gc, path, transform, rgbFace=None): path = transform.transform_path(path) points = path.vertices codes = path.codes bbox = gc.get_clip_rectangle() if bbox is not None and not np.any(np.isnan(bbox.bounds)): x, y, w, h = bbox.bounds clrt = np.array([x, x + w, y, y + h]) else: clrt = np.array([0, self.width, 0, self.height]) gr.setviewport(*clrt / self.size) gr.setwindow(*clrt) if rgbFace is not None and len(points) > 2: color = gr.inqcolorfromrgb(rgbFace[0], rgbFace[1], rgbFace[2]) gr.settransparency(rgbFace[3]) gr.setcolorrep(color, rgbFace[0], rgbFace[1], rgbFace[2]) gr.setfillintstyle(gr.INTSTYLE_SOLID) gr.setfillcolorind(color) gr.drawpath(points, codes, fill=True) lw = gc.get_linewidth() if lw != 0: rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) if isinstance(gc._linestyle, str): gr.setlinetype(linetype[gc._linestyle]) gr.setlinewidth(lw) gr.setlinecolorind(color) gr.drawpath(points, codes, fill=False) def draw_image(self, gc, x, y, im): if hasattr(im, 'as_rgba_str'): h, w, s = im.as_rgba_str() img = np.fromstring(s, np.uint32) img.shape = (h, w) elif len(im.shape) == 3 and im.shape[2] == 4 and im.dtype == np.uint8: img = im.view(np.uint32) img.shape = im.shape[:2] h, w = img.shape else: type_info = repr(type(im)) if hasattr(im, 'shape'): type_info += ' shape=' + repr(im.shape) if hasattr(im, 'dtype'): type_info += ' dtype=' + repr(im.dtype) warnings.warn('Unsupported image type ({}). Please report this at https://github.com/sciapp/python-gr/issues'.format(type_info)) return gr.drawimage(x, x + w, y + h, y, w, h, img) def draw_mathtext(self, x, y, angle, Z): h, w = Z.shape img = np.zeros((h, w), np.uint32) for i in range(h): for j in range(w): img[i, j] = (255 - Z[i, j]) << 24 a = int(angle) if a == 90: gr.drawimage(x - h, x, y, y + w, h, w, np.resize(np.rot90(img, 1), (h, w))) elif a == 180: gr.drawimage(x - w, x, y - h, y, w, h, np.rot90(img, 2)) elif a == 270: gr.drawimage(x, x + h, y - w, y, h, w, np.resize(np.rot90(img, 3), (h, w))) else: gr.drawimage(x, x + w, y, y + h, w, h, img) def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): size = prop.get_size_in_points() key = s, size, self.dpi, angle, self.texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = self.texmanager.get_grey(s, size, self.dpi) Z = np.array(255.0 - Z * 255.0, np.uint8) self.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) self.draw_mathtext(x, y, angle, 255 - np.asarray(image)) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: x, y = gr.wctondc(x, y) fontsize = prop.get_size_in_points() rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) gr.setcharheight(fontsize * self.nominal_fontsize) gr.settextcolorind(color) if angle != 0: gr.setcharup(-np.sin(angle * np.pi/180), np.cos(angle * np.pi/180)) else: gr.setcharup(0, 1) gr.text(x, y, s) def flipy(self): return False def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': fontsize = prop.get_size_in_points() w, h, d = self.texmanager.get_text_width_height_descent( s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent # family = prop.get_family() # weight = prop.get_weight() # style = prop.get_style() fontsize = prop.get_size_in_points() gr.setcharheight(fontsize * self.nominal_fontsize) gr.setcharup(0, 1) (tbx, tby) = gr.inqtextext(0, 0, s) width, height, descent = tbx[1], tby[2], 0.2 * tby[2] return width, height, descent def new_gc(self): return GraphicsContextGR() def points_to_pixels(self, points): return points
class RendererPS(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ fontd = maxdict(50) afmfontd = maxdict(50) def __init__(self, width, height, pswriter, imagedpi=72): """ Although postscript itself is dpi independent, we need to imform the image code about a requested dpi to generate high res images and them scale them before embeddin them """ RendererBase.__init__(self) self.width = width self.height = height self._pswriter = pswriter if rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi if rcParams['path.simplify']: self.simplify = (width * imagedpi, height * imagedpi) else: self.simplify = None # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self._hatches = {} self.image_magnification = imagedpi/72.0 self._clip_paths = {} self._path_collection_id = 0 self.used_characters = {} self.mathtext_parser = MathTextParser("PS") def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" realpath, stat_key = get_realpath_and_stat(font.fname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update([ord(x) for x in s]) def merge_used_characters(self, other): for stat_key, (realpath, charset) in other.items(): used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update(charset) def set_color(self, r, g, b, store=1): if (r,g,b) != self.color: if r==g and r==b: self._pswriter.write("%1.3f setgray\n"%r) else: self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n"%(r,g,b)) if store: self.color = (r,g,b) def set_linewidth(self, linewidth, store=1): if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n"%linewidth) if store: self.linewidth = linewidth def set_linejoin(self, linejoin, store=1): if linejoin != self.linejoin: self._pswriter.write("%d setlinejoin\n"%linejoin) if store: self.linejoin = linejoin def set_linecap(self, linecap, store=1): if linecap != self.linecap: self._pswriter.write("%d setlinecap\n"%linecap) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=1): if self.linedash is not None: oldo, oldseq = self.linedash if seq_allequal(seq, oldseq): return if seq is not None and len(seq): s="[%s] %d setdash\n"%(_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset,seq) def set_font(self, fontname, fontsize, store=1): if rcParams['ps.useafm']: return if (fontname,fontsize) != (self.fontname,self.fontsize): out = ("/%s findfont\n" "%1.3f scalefont\n" "setfont\n" % (fontname,fontsize)) self._pswriter.write(out) if store: self.fontname = fontname if store: self.fontsize = fontsize def create_hatch(self, hatch): sidelen = 72 if self._hatches.has_key(hatch): return self._hatches[hatch] name = 'H%d' % len(self._hatches) self._pswriter.write("""\ << /PatternType 1 /PaintType 2 /TilingType 2 /BBox[0 0 %(sidelen)d %(sidelen)d] /XStep %(sidelen)d /YStep %(sidelen)d /PaintProc { pop 0 setlinewidth """ % locals()) self._pswriter.write( self._convert_path(Path.hatch(hatch), Affine2D().scale(72.0))) self._pswriter.write("""\ stroke } bind >> matrix makepattern /%(name)s exch def """ % locals()) self._hatches[hatch] = name return name def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop """ if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent if rcParams['ps.useafm']: if ismath: s = s[1:-1] font = self._get_font_afm(prop) l,b,w,h,d = font.get_str_bbox_and_descent(s) fontsize = prop.get_size_in_points() scale = 0.001*fontsize w *= scale h *= scale d *= scale return w, h, d font = self._get_font_ttf(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 #print s, w, h return w, h, d def flipy(self): 'return true if small y numbers are top for renderer' return False def _get_font_afm(self, prop): key = hash(prop) font = self.afmfontd.get(key) if font is None: fname = findfont(prop, fontext='afm') font = self.afmfontd.get(fname) if font is None: font = AFM(file(findfont(prop, fontext='afm'))) self.afmfontd[fname] = font self.afmfontd[key] = font return font def _get_font_ttf(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _rgba(self, im): return im.as_rgba_str() def _rgb(self, im): h,w,s = im.as_rgba_str() rgba = npy.fromstring(s, npy.uint8) rgba.shape = (h, w, 4) rgb = rgba[:,:,:3] return h, w, rgb.tostring() def _gray(self, im, rc=0.3, gc=0.59, bc=0.11): rgbat = im.as_rgba_str() rgba = npy.fromstring(rgbat[2], npy.uint8) rgba.shape = (rgbat[0], rgbat[1], 4) rgba_f = rgba.astype(npy.float32) r = rgba_f[:,:,0] g = rgba_f[:,:,1] b = rgba_f[:,:,2] gray = (r*rc + g*gc + b*bc).astype(npy.uint8) return rgbat[0], rgbat[1], gray.tostring() def _hex_lines(self, s, chars_per_line=128): s = binascii.b2a_hex(s) nhex = len(s) lines = [] for i in range(0,nhex,chars_per_line): limit = min(i+chars_per_line, nhex) lines.append(s[i:limit]) return lines def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): """ Draw the Image instance into the current axes; x is the distance in pixels from the left hand side of the canvas and y is the distance from bottom bbox is a matplotlib.transforms.BBox instance for clipping, or None """ im.flipud_out() if im.is_grayscale: h, w, bits = self._gray(im) imagecmd = "image" else: h, w, bits = self._rgb(im) imagecmd = "false 3 colorimage" hexlines = '\n'.join(self._hex_lines(bits)) xscale, yscale = ( w/self.image_magnification, h/self.image_magnification) figh = self.height*72 #print 'values', origin, flipud, figh, h, y clip = [] if bbox is not None: clipx,clipy,clipw,cliph = bbox.bounds clip.append('%s clipbox' % _nums_to_str(clipw, cliph, clipx, clipy)) if clippath is not None: id = self._get_clip_path(clippath, clippath_trans) clip.append('%s' % id) clip = '\n'.join(clip) #y = figh-(y+h) ps = """gsave %(clip)s %(x)s %(y)s translate %(xscale)s %(yscale)s scale /DataString %(w)s string def %(w)s %(h)s 8 [ %(w)s 0 0 -%(h)s 0 %(h)s ] { currentfile DataString readhexstring pop } bind %(imagecmd)s %(hexlines)s grestore """ % locals() self._pswriter.write(ps) # unflip im.flipud_out() def _convert_path(self, path, transform, simplify=None): path = transform.transform_path(path) ps = [] last_points = None for points, code in path.iter_segments(simplify): if code == Path.MOVETO: ps.append("%g %g m" % tuple(points)) elif code == Path.LINETO: ps.append("%g %g l" % tuple(points)) elif code == Path.CURVE3: points = quad2cubic(*(list(last_points[-2:]) + list(points))) ps.append("%g %g %g %g %g %g c" % tuple(points[2:])) elif code == Path.CURVE4: ps.append("%g %g %g %g %g %g c" % tuple(points)) elif code == Path.CLOSEPOLY: ps.append("cl") last_points = points ps = "\n".join(ps) return ps def _get_clip_path(self, clippath, clippath_transform): id = self._clip_paths.get((clippath, clippath_transform)) if id is None: id = 'c%x' % len(self._clip_paths) ps_cmd = ['/%s {' % id] ps_cmd.append(self._convert_path(clippath, clippath_transform)) ps_cmd.extend(['clip', 'newpath', '} bind def\n']) self._pswriter.write('\n'.join(ps_cmd)) self._clip_paths[(clippath, clippath_transform)] = id return id def draw_path(self, gc, path, transform, rgbFace=None): """ Draws a Path instance using the given affine transform. """ ps = self._convert_path(path, transform, self.simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): """ Draw the markers defined by path at each of the positions in x and y. path coordinates are points, x and y coords will be transformed by the transform """ if debugPS: self._pswriter.write('% draw_markers \n') write = self._pswriter.write if rgbFace: if rgbFace[0]==rgbFace[1] and rgbFace[0]==rgbFace[2]: ps_color = '%1.3f setgray' % rgbFace[0] else: ps_color = '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace # construct the generic marker command: ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] # dont want the translate to be global ps_cmd.append(self._convert_path(marker_path, marker_trans)) if rgbFace: ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore']) ps_cmd.extend(['stroke', 'grestore', '} bind def']) tpath = trans.transform_path(path) for vertices, code in tpath.iter_segments(): if len(vertices): x, y = vertices[-2:] ps_cmd.append("%g %g o" % (x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._pswriter.write path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): name = 'p%x_%x' % (self._path_collection_id, i) ps_cmd = ['/%s {' % name, 'newpath', 'translate'] ps_cmd.append(self._convert_path(path, transform)) ps_cmd.extend(['} bind def\n']) write('\n'.join(ps_cmd)) path_codes.append(name) for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'): """ draw a Text instance """ w, h, bl = self.get_text_width_height_descent(s, prop, ismath) fontsize = prop.get_size_in_points() corr = 0#w/2*(fontsize-10)/10 pos = _nums_to_str(x-corr, y) thetext = 'psmarker%d' % self.textcnt color = '%1.3f,%1.3f,%1.3f'% gc.get_rgb()[:3] fontcmd = {'sans-serif' : r'{\sffamily %s}', 'monospace' : r'{\ttfamily %s}'}.get( rcParams['font.family'], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) self.psfrag.append(r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}'%(thetext, angle, fontsize, fontsize*1.25, tex)) ps = """\ gsave %(pos)s moveto (%(thetext)s) show grestore """ % locals() self._pswriter.write(ps) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath): """ draw a Text instance """ # local to avoid repeated attribute lookups write = self._pswriter.write if debugPS: write("% text\n") if ismath=='TeX': return self.tex(gc, x, y, s, prop, angle) elif ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) elif isinstance(s, unicode): return self.draw_unicode(gc, x, y, s, prop, angle) elif rcParams['ps.useafm']: font = self._get_font_afm(prop) l,b,w,h = font.get_str_bbox(s) fontsize = prop.get_size_in_points() l *= 0.001*fontsize b *= 0.001*fontsize w *= 0.001*fontsize h *= 0.001*fontsize if angle==90: l,b = -b, l # todo generalize for arb rotations pos = _nums_to_str(x-l, y-b) thetext = '(%s)' % s fontname = font.get_fontname() fontsize = prop.get_size_in_points() rotate = '%1.1f rotate' % angle setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()[:3] #h = 0 ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(pos)s moveto %(rotate)s %(thetext)s %(setcolor)s show grestore """ % locals() self._draw_ps(ps, gc, None) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) write("%s m\n"%_nums_to_str(x,y)) if angle: write("gsave\n") write("%s rotate\n"%_num_to_str(angle)) descent = font.get_descent() / 64.0 if descent: write("0 %s rmoveto\n"%_num_to_str(descent)) write("(%s) show\n"%quote_ps_string(s)) if angle: write("grestore\n") def new_gc(self): return GraphicsContextPS() def draw_unicode(self, gc, x, y, s, prop, angle): """draw a unicode string. ps doesn't have unicode support, so we have to do this the hard way """ if rcParams['ps.useafm']: self.set_color(*gc.get_rgb()) font = self._get_font_afm(prop) fontname = font.get_fontname() fontsize = prop.get_size_in_points() scale = 0.001*fontsize thisx = 0 thisy = font.get_str_bbox_and_descent(s)[4] * scale last_name = None lines = [] for c in s: name = uni2type1.get(ord(c), 'question') try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') if last_name is not None: kern = font.get_kern_dist_from_name(last_name, name) else: kern = 0 last_name = name thisx += kern * scale lines.append('%f %f m /%s glyphshow'%(thisx, thisy, name)) thisx += width * scale thetext = "\n".join(lines) ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) cmap = font.get_charmap() lastgind = None #print 'text', s lines = [] thisx = 0 thisy = font.get_descent() / 64.0 for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') name = '.notdef' gind = 0 else: name = font.get_glyph_name(gind) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind thisx += kern/64.0 lines.append('%f %f m /%s glyphshow'%(thisx, thisy, name)) thisx += glyph.linearHoriAdvance/65536.0 thetext = '\n'.join(lines) ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) self.merge_used_characters(used_characters) self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript sniplet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% "+command+"\n") mightstroke = (gc.get_linewidth() > 0.0 and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0)) stroke = stroke and mightstroke fill = (fill and rgbFace is not None and (len(rgbFace) <= 3 or rgbFace[3] != 0.0)) if mightstroke: self.set_linewidth(gc.get_linewidth()) jint = gc.get_joinstyle() self.set_linejoin(jint) cint = gc.get_capstyle() self.set_linecap(cint) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') cliprect = gc.get_clip_rectangle() if cliprect: x,y,w,h=cliprect.bounds write('%1.4g %1.4g %1.4g %1.4g clipbox\n' % (w,h,x,y)) clippath, clippath_trans = gc.get_clip_path() if clippath: id = self._get_clip_path(clippath, clippath_trans) write('%s\n' % id) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke: write("gsave\n") self.set_color(store=0, *rgbFace[:3]) write("fill\n") if stroke: write("grestore\n") hatch = gc.get_hatch() if hatch: hatch_name = self.create_hatch(hatch) write("gsave\n") write("[/Pattern [/DeviceRGB]] setcolorspace %f %f %f " % gc.get_rgb()[:3]) write("%s setcolor fill grestore\n" % hatch_name) if stroke: write("stroke\n") write("grestore\n")
class RendererPS(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ fontd = maxdict(50) afmfontd = maxdict(50) def __init__(self, width, height, pswriter, imagedpi=72): """ Although postscript itself is dpi independent, we need to imform the image code about a requested dpi to generate high res images and them scale them before embeddin them """ RendererBase.__init__(self) self.width = width self.height = height self._pswriter = pswriter if rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi if rcParams['path.simplify']: self.simplify = (width * imagedpi, height * imagedpi) else: self.simplify = None # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self.hatch = None self.image_magnification = imagedpi/72.0 self._clip_paths = {} self._path_collection_id = 0 self.used_characters = {} self.mathtext_parser = MathTextParser("PS") def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" realpath, stat_key = get_realpath_and_stat(font.fname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update([ord(x) for x in s]) def merge_used_characters(self, other): for stat_key, (realpath, charset) in other.items(): used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update(charset) def set_color(self, r, g, b, store=1): if (r,g,b) != self.color: if r==g and r==b: self._pswriter.write("{0:1.3f} setgray\n".format(r)) else: self._pswriter.write("{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor\n".format(r, g, b)) if store: self.color = (r,g,b) def set_linewidth(self, linewidth, store=1): if linewidth != self.linewidth: self._pswriter.write("{0:1.3f} setlinewidth\n".format(linewidth)) if store: self.linewidth = linewidth def set_linejoin(self, linejoin, store=1): if linejoin != self.linejoin: self._pswriter.write("{0:d} setlinejoin\n".format(linejoin)) if store: self.linejoin = linejoin def set_linecap(self, linecap, store=1): if linecap != self.linecap: self._pswriter.write("{0:d} setlinecap\n".format(linecap)) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=1): if self.linedash is not None: oldo, oldseq = self.linedash if seq_allequal(seq, oldseq): return if seq is not None and len(seq): s="[{0!s}] {1:d} setdash\n".format(_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset,seq) def set_font(self, fontname, fontsize, store=1): if rcParams['ps.useafm']: return if (fontname,fontsize) != (self.fontname,self.fontsize): out = ("/%s findfont\n" "%1.3f scalefont\n" "setfont\n" % (fontname,fontsize)) self._pswriter.write(out) if store: self.fontname = fontname if store: self.fontsize = fontsize def set_hatch(self, hatch): """ hatch can be one of: / - diagonal hatching \ - back diagonal | - vertical - - horizontal + - crossed X - crossed diagonal letters can be combined, in which case all the specified hatchings are done if same letter repeats, it increases the density of hatching in that direction """ hatches = {'horiz':0, 'vert':0, 'diag1':0, 'diag2':0} for letter in hatch: if (letter == '/'): hatches['diag2'] += 1 elif (letter == '\\'): hatches['diag1'] += 1 elif (letter == '|'): hatches['vert'] += 1 elif (letter == '-'): hatches['horiz'] += 1 elif (letter == '+'): hatches['horiz'] += 1 hatches['vert'] += 1 elif (letter.lower() == 'x'): hatches['diag1'] += 1 hatches['diag2'] += 1 def do_hatch(angle, density): if (density == 0): return "" return """\ gsave eoclip {0!s} rotate 0.0 0.0 0.0 0.0 setrgbcolor 0 setlinewidth /hatchgap {1:d} def pathbbox /hatchb exch def /hatchr exch def /hatcht exch def /hatchl exch def hatchl cvi hatchgap idiv hatchgap mul hatchgap hatchr cvi hatchgap idiv hatchgap mul {{hatcht m 0 hatchb hatcht sub r }} for stroke grestore """.format(angle, 12/density) self._pswriter.write("gsave\n") self._pswriter.write(do_hatch(90, hatches['horiz'])) self._pswriter.write(do_hatch(0, hatches['vert'])) self._pswriter.write(do_hatch(45, hatches['diag1'])) self._pswriter.write(do_hatch(-45, hatches['diag2'])) self._pswriter.write("grestore\n") def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop """ if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() l,b,r,t = texmanager.get_ps_bbox(s, fontsize) w = (r-l) h = (t-b) # TODO: We need a way to get a good baseline from # text.usetex return w, h, 0 if ismath: width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent if rcParams['ps.useafm']: if ismath: s = s[1:-1] font = self._get_font_afm(prop) l,b,w,h,d = font.get_str_bbox_and_descent(s) fontsize = prop.get_size_in_points() scale = 0.001*fontsize w *= scale h *= scale d *= scale return w, h, d font = self._get_font_ttf(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 #print s, w, h return w, h, d def flipy(self): 'return true if small y numbers are top for renderer' return False def _get_font_afm(self, prop): key = hash(prop) font = self.afmfontd.get(key) if font is None: fname = findfont(prop, fontext='afm') font = self.afmfontd.get(fname) if font is None: font = AFM(file(findfont(prop, fontext='afm'))) self.afmfontd[fname] = font self.afmfontd[key] = font return font def _get_font_ttf(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _rgba(self, im): return im.as_rgba_str() def _rgb(self, im): h,w,s = im.as_rgba_str() rgba = npy.fromstring(s, npy.uint8) rgba.shape = (h, w, 4) rgb = rgba[:,:,:3] return h, w, rgb.tostring() def _gray(self, im, rc=0.3, gc=0.59, bc=0.11): rgbat = im.as_rgba_str() rgba = npy.fromstring(rgbat[2], npy.uint8) rgba.shape = (rgbat[0], rgbat[1], 4) rgba_f = rgba.astype(npy.float32) r = rgba_f[:,:,0] g = rgba_f[:,:,1] b = rgba_f[:,:,2] gray = (r*rc + g*gc + b*bc).astype(npy.uint8) return rgbat[0], rgbat[1], gray.tostring() def _hex_lines(self, s, chars_per_line=128): s = binascii.b2a_hex(s) nhex = len(s) lines = [] for i in range(0,nhex,chars_per_line): limit = min(i+chars_per_line, nhex) lines.append(s[i:limit]) return lines def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): """ Draw the Image instance into the current axes; x is the distance in pixels from the left hand side of the canvas and y is the distance from bottom bbox is a matplotlib.transforms.BBox instance for clipping, or None """ im.flipud_out() if im.is_grayscale: h, w, bits = self._gray(im) imagecmd = "image" else: h, w, bits = self._rgb(im) imagecmd = "false 3 colorimage" hexlines = '\n'.join(self._hex_lines(bits)) xscale, yscale = ( w/self.image_magnification, h/self.image_magnification) figh = self.height*72 #print 'values', origin, flipud, figh, h, y clip = [] if bbox is not None: clipx,clipy,clipw,cliph = bbox.bounds clip.append('{0!s} clipbox'.format(_nums_to_str(clipw, cliph, clipx, clipy))) if clippath is not None: id = self._get_clip_path(clippath, clippath_trans) clip.append('{0!s}'.format(id)) clip = '\n'.join(clip) #y = figh-(y+h) ps = """gsave {clip!s} {x!s} {y!s} translate {xscale!s} {yscale!s} scale /DataString {w!s} string def {w!s} {h!s} 8 [ {w!s} 0 0 -{h!s} 0 {h!s} ] {{ currentfile DataString readhexstring pop }} bind {imagecmd!s} {hexlines!s} grestore """.format(**locals()) self._pswriter.write(ps) # unflip im.flipud_out() def _convert_path(self, path, transform, simplify=None): path = transform.transform_path(path) ps = [] last_points = None for points, code in path.iter_segments(simplify): if code == Path.MOVETO: ps.append("{0:g} {1:g} m".format(*tuple(points))) elif code == Path.LINETO: ps.append("{0:g} {1:g} l".format(*tuple(points))) elif code == Path.CURVE3: points = quad2cubic(*(list(last_points[-2:]) + list(points))) ps.append("{0:g} {1:g} {2:g} {3:g} {4:g} {5:g} c".format(* tuple(points[2:]))) elif code == Path.CURVE4: ps.append("{0:g} {1:g} {2:g} {3:g} {4:g} {5:g} c".format(*tuple(points))) elif code == Path.CLOSEPOLY: ps.append("cl") last_points = points ps = "\n".join(ps) return ps def _get_clip_path(self, clippath, clippath_transform): id = self._clip_paths.get((clippath, clippath_transform)) if id is None: id = 'c{0:x}'.format(len(self._clip_paths)) ps_cmd = ['/{0!s} {{'.format(id)] ps_cmd.append(self._convert_path(clippath, clippath_transform)) ps_cmd.extend(['clip', 'newpath', '} bind def\n']) self._pswriter.write('\n'.join(ps_cmd)) self._clip_paths[(clippath, clippath_transform)] = id return id def draw_path(self, gc, path, transform, rgbFace=None): """ Draws a Path instance using the given affine transform. """ ps = self._convert_path(path, transform, self.simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): """ Draw the markers defined by path at each of the positions in x and y. path coordinates are points, x and y coords will be transformed by the transform """ if debugPS: self._pswriter.write('% draw_markers \n') write = self._pswriter.write if rgbFace: if rgbFace[0]==rgbFace[1] and rgbFace[0]==rgbFace[2]: ps_color = '{0:1.3f} setgray'.format(rgbFace[0]) else: ps_color = '{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor'.format(*rgbFace) # construct the generic marker command: ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] # dont want the translate to be global ps_cmd.append(self._convert_path(marker_path, marker_trans)) if rgbFace: ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore']) ps_cmd.extend(['stroke', 'grestore', '} bind def']) tpath = trans.transform_path(path) for vertices, code in tpath.iter_segments(): if len(vertices): x, y = vertices[-2:] ps_cmd.append("{0:g} {1:g} o".format(x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._pswriter.write path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): name = 'p{0:x}_{1:x}'.format(self._path_collection_id, i) ps_cmd = ['/{0!s} {{'.format(name), 'newpath', 'translate'] ps_cmd.append(self._convert_path(path, transform)) ps_cmd.extend(['} bind def\n']) write('\n'.join(ps_cmd)) path_codes.append(name) for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): ps = "{0:g} {1:g} {2!s}".format(xo, yo, path_id) self._draw_ps(ps, gc, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'): """ draw a Text instance """ w, h, bl = self.get_text_width_height_descent(s, prop, ismath) fontsize = prop.get_size_in_points() corr = 0#w/2*(fontsize-10)/10 pos = _nums_to_str(x-corr, y) thetext = 'psmarker{0:d}'.format(self.textcnt) color = '{0:1.3f},{1:1.3f},{2:1.3f}'.format(*gc.get_rgb()[:3]) fontcmd = {'sans-serif' : r'{\sffamily %s}', 'monospace' : r'{\ttfamily %s}'}.get( rcParams['font.family'], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{{{0!s}}} {1!s}'.format(color, s) self.psfrag.append(r'\psfrag{{{0!s}}}[bl][bl][1][{1:f}]{{\fontsize{{{2:f}}}{{{3:f}}}{4!s}}}'.format(thetext, angle, fontsize, fontsize*1.25, tex)) ps = """\ gsave {pos!s} moveto ({thetext!s}) show grestore """.format(**locals()) self._pswriter.write(ps) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath): """ draw a Text instance """ # local to avoid repeated attribute lookups write = self._pswriter.write if debugPS: write("% text\n") if ismath=='TeX': return self.tex(gc, x, y, s, prop, angle) elif ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) elif isinstance(s, unicode): return self.draw_unicode(gc, x, y, s, prop, angle) elif rcParams['ps.useafm']: font = self._get_font_afm(prop) l,b,w,h = font.get_str_bbox(s) fontsize = prop.get_size_in_points() l *= 0.001*fontsize b *= 0.001*fontsize w *= 0.001*fontsize h *= 0.001*fontsize if angle==90: l,b = -b, l # todo generalize for arb rotations pos = _nums_to_str(x-l, y-b) thetext = '({0!s})'.format(s) fontname = font.get_fontname() fontsize = prop.get_size_in_points() rotate = '{0:1.1f} rotate'.format(angle) setcolor = '{0:1.3f} {1:1.3f} {2:1.3f} setrgbcolor'.format(*gc.get_rgb()[:3]) #h = 0 ps = """\ gsave /{fontname!s} findfont {fontsize!s} scalefont setfont {pos!s} moveto {rotate!s} {thetext!s} {setcolor!s} show grestore """.format(**locals()) self._draw_ps(ps, gc, None) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) write("{0!s} m\n".format(_nums_to_str(x,y))) if angle: write("gsave\n") write("{0!s} rotate\n".format(_num_to_str(angle))) descent = font.get_descent() / 64.0 if descent: write("0 {0!s} rmoveto\n".format(_num_to_str(descent))) write("({0!s}) show\n".format(quote_ps_string(s))) if angle: write("grestore\n") def new_gc(self): return GraphicsContextPS() def draw_unicode(self, gc, x, y, s, prop, angle): """draw a unicode string. ps doesn't have unicode support, so we have to do this the hard way """ if rcParams['ps.useafm']: self.set_color(*gc.get_rgb()) font = self._get_font_afm(prop) fontname = font.get_fontname() fontsize = prop.get_size_in_points() scale = 0.001*fontsize thisx = 0 thisy = font.get_str_bbox_and_descent(s)[4] * scale last_name = None lines = [] for c in s: name = uni2type1.get(ord(c), 'question') try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') if last_name is not None: kern = font.get_kern_dist_from_name(last_name, name) else: kern = 0 last_name = name thisx += kern * scale lines.append('{0:f} {1:f} m /{2!s} glyphshow'.format(thisx, thisy, name)) thisx += width * scale thetext = "\n".join(lines) ps = """\ gsave /{fontname!s} findfont {fontsize!s} scalefont setfont {x:f} {y:f} translate {angle:f} rotate {thetext!s} grestore """.format(**locals()) self._pswriter.write(ps) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points()) cmap = font.get_charmap() lastgind = None #print 'text', s lines = [] thisx = 0 thisy = font.get_descent() / 64.0 for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') name = '.notdef' gind = 0 else: name = font.get_glyph_name(gind) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind thisx += kern/64.0 lines.append('{0:f} {1:f} m /{2!s} glyphshow'.format(thisx, thisy, name)) thisx += glyph.linearHoriAdvance/65536.0 thetext = '\n'.join(lines) ps = """gsave {x:f} {y:f} translate {angle:f} rotate {thetext!s} grestore """.format(**locals()) self._pswriter.write(ps) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) self.merge_used_characters(used_characters) self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() ps = """gsave {x:f} {y:f} translate {angle:f} rotate {thetext!s} grestore """.format(**locals()) self._pswriter.write(ps) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript sniplet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% "+command+"\n") mightstroke = (gc.get_linewidth() > 0.0 and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0)) stroke = stroke and mightstroke fill = (fill and rgbFace is not None and (len(rgbFace) <= 3 or rgbFace[3] != 0.0)) if mightstroke: self.set_linewidth(gc.get_linewidth()) jint = gc.get_joinstyle() self.set_linejoin(jint) cint = gc.get_capstyle() self.set_linecap(cint) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') cliprect = gc.get_clip_rectangle() if cliprect: x,y,w,h=cliprect.bounds write('{0:1.4g} {1:1.4g} {2:1.4g} {3:1.4g} clipbox\n'.format(w, h, x, y)) clippath, clippath_trans = gc.get_clip_path() if clippath: id = self._get_clip_path(clippath, clippath_trans) write('{0!s}\n'.format(id)) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke: write("gsave\n") self.set_color(store=0, *rgbFace[:3]) write("fill\ngrestore\n") else: self.set_color(store=0, *rgbFace[:3]) write("fill\n") hatch = gc.get_hatch() if hatch: self.set_hatch(hatch) if stroke: write("stroke\n") write("grestore\n")
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 RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug = 1 def __init__(self, width, height, dpi): if __debug__: verbose.report("RendererAgg.__init__", "debug-annoying") RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report("RendererAgg.__init__ width=%s, height=%s" % (width, height), "debug-annoying") self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report("RendererAgg.__init__ _RendererAgg done", "debug-annoying") self._update_methods() self.mathtext_parser = MathTextParser("Agg") self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report("RendererAgg.__init__ done", "debug-annoying") def _get_hinting_flag(self): if rcParams["text.hinting"]: return LOAD_FORCE_AUTOHINT else: return LOAD_NO_HINTING def draw_markers(self, *kl, **kw): # for filtering to work with rastrization, methods needs to be wrapped. # maybe there is better way to do it. return self._renderer.draw_markers(*kl, **kw) def _update_methods(self): # self.draw_path = self._renderer.draw_path # see below # self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams["agg.path.chunksize"] # here at least for testing npts = path.vertices.shape[0] if nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None: nch = np.ceil(npts / float(nmax)) chsize = int(np.ceil(npts / nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1, :] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report("RendererAgg.draw_mathtext", "debug-annoying") ox, oy, width, height, descent, font_image, used_characters = self.mathtext_parser.parse(s, self.dpi, prop) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report("RendererAgg.draw_text", "debug-annoying") if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = self._get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=flags) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap() # print x, y, int(x), int(y), s self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make cacheing in the # texmanager more efficient. It is not meant to be used # outside the backend """ if rcParams["text.usetex"]: # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = self._get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): "return the canvas width and height in display coords" return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report("RendererAgg._get_agg_font", "debug-annoying") key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report("RendererAgg.points_to_pixels", "debug-annoying") return points * self.dpi / 72.0 def tostring_rgb(self): if __debug__: verbose.report("RendererAgg.tostring_rgb", "debug-annoying") return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report("RendererAgg.tostring_argb", "debug-annoying") return self._renderer.tostring_argb() def buffer_rgba(self, x, y): if __debug__: verbose.report("RendererAgg.buffer_rgba", "debug-annoying") return self._renderer.buffer_rgba(x, y) def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): """ agg backend support arbitrary scaling of image. """ return True def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy self._renderer.restore_region2(region, x1, y1, x2, y2, ox, oy) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ from matplotlib._image import fromarray width, height = int(self.width), int(self.height) buffer, bounds = self._renderer.tostring_rgba_minimized() l, b, w, h = bounds self._renderer = self._filter_renderers.pop() self._update_methods() if w > 0 and h > 0: img = np.fromstring(buffer, np.uint8) img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.0, self.dpi) image = fromarray(img, 1) image.flipud_out() gc = self.new_gc() self._renderer.draw_image(gc, l + ox, height - b - h + oy, image)
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.dpi = dpi self.gc = GraphicsContextCairo (renderer=self) self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface (self, surface): self.gc.ctx = cairo.Context (surface) def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: ctx.save() if len(fill_c) == 3 or alpha_overrides: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod def convert_path(ctx, path, transform, clip=None): for points, code in path.iter_segments(transform, clip=clip): if code == Path.MOVETO: ctx.move_to(*points) elif code == Path.CLOSEPOLY: ctx.close_path() elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: ctx.curve_to(points[0], points[1], points[0], points[1], points[2], points[3]) elif code == Path.CURVE4: ctx.curve_to(*points) def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx # We'll clip the path to the actual rendering extents # if the path isn't filled. if rgbFace is None and gc.get_hatch() is None: clip = ctx.clip_extents() else: clip = None transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) ctx.new_path() self.convert_path(ctx, path, transform, clip) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None): ctx = gc.ctx ctx.new_path() # Create the path for the marker; it needs to be flipped here already! self.convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill x1, y1, x2, y2 = ctx.fill_extents() if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0: filled = False # No fill, just unset this (so we don't try to fill it later on) rgbFace = None else: filled = True transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) ctx.new_path() for i, (vertices, codes) in enumerate(path.iter_segments(transform, simplify=False)): if len(vertices): x, y = vertices[-2:] ctx.save() # Translate and apply path ctx.translate(x, y) ctx.append_path(marker_path) ctx.restore() # Slower code path if there is a fill; we need to draw # the fill and stroke for each marker at the same time. # Also flush out the drawing every once in a while to # prevent the paths from getting way too long. if filled or i % 1000 == 0: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) # Fast path, if there is no fill, draw everything in one step if not filled: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_image(self, gc, x, y, im): # bbox - not currently used if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if sys.byteorder == 'little': im = im[:, :, (2, 1, 0, 3)] else: im = im[:, :, (3, 0, 1, 2)] if HAS_CAIRO_CFFI: # cairocffi tries to use the buffer_info from array.array # that we replicate in ArrayWrapper and alternatively falls back # on ctypes to get a pointer to the numpy array. This works # correctly on a numpy array in python3 but not 2.7. We replicate # the array.array functionality here to get cross version support. imbuffer = ArrayWrapper(im.flatten()) else: # py2cairo uses PyObject_AsWriteBuffer # to get a pointer to the numpy array this works correctly # on a regular numpy array but not on a memory view. # At the time of writing the latest release version of # py3cairo still does not support create_for_data imbuffer = im.flatten() surface = cairo.ImageSurface.create_for_data(imbuffer, cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1]*4) ctx = gc.ctx y = self.height - y - im.shape[0] ctx.save() ctx.set_source_surface(surface, float(x), float(y)) if gc.get_alpha() != 1.0: ctx.paint_with_alpha(gc.get_alpha()) else: ctx.paint() ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate (-angle * np.pi / 180) ctx.set_font_size (size) if HAS_CAIRO_CFFI: if not isinstance(s, six.text_type): s = six.text_type(s) else: if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate (-angle * np.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle (ox, oy, w, h) ctx.set_source_rgb (0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size (size) y_bearing, w, h = ctx.text_extents (s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.gc.ctx.save() self.gc._alpha = 1.0 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return points/72.0 * self.dpi
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug = 1 texd = maxdict(50) # a cache of tex image rasters _fontd = maxdict(50) def __init__(self, width, height, dpi): if __debug__: verbose.report("RendererAgg.__init__", "debug-annoying") RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report( "RendererAgg.__init__ width=%s, \ height=%s" % (width, height), "debug-annoying", ) self._renderer = _RendererAgg(int(width), int(height), dpi.get(), debug=False) if __debug__: verbose.report("RendererAgg.__init__ _RendererAgg done", "debug-annoying") self.draw_polygon = self._renderer.draw_polygon self.draw_rectangle = self._renderer.draw_rectangle self.draw_path = self._renderer.draw_path self.draw_lines = self._renderer.draw_lines self.draw_markers = self._renderer.draw_markers self.draw_image = self._renderer.draw_image self.draw_line_collection = self._renderer.draw_line_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_poly_collection = self._renderer.draw_poly_collection self.draw_regpoly_collection = self._renderer.draw_regpoly_collection self.copy_from_bbox = self._renderer.copy_from_bbox self.restore_region = self._renderer.restore_region self.mathtext_parser = MathTextParser("Agg") self.bbox = lbwh_to_bbox(0, 0, self.width, self.height) if __debug__: verbose.report("RendererAgg.__init__ done", "debug-annoying") def draw_arc(self, gcEdge, rgbFace, x, y, width, height, angle1, angle2, rotation): """ Draw an arc centered at x,y with width and height and angles from 0.0 to 360.0 If rgbFace is not None, fill the rectangle with that color. gcEdge is a GraphicsContext instance Currently, I'm only supporting ellipses, ie angle args are ignored """ if __debug__: verbose.report("RendererAgg.draw_arc", "debug-annoying") self._renderer.draw_ellipse(gcEdge, rgbFace, x, y, width / 2.0, height / 2.0, rotation) # ellipse takes radius def draw_line(self, gc, x1, y1, x2, y2): """ x and y are equal length arrays, draw lines connecting each point in x, y """ if __debug__: verbose.report("RendererAgg.draw_line", "debug-annoying") x = npy.array([x1, x2], float) y = npy.array([y1, y2], float) self._renderer.draw_lines(gc, x, y) def draw_point(self, gc, x, y): """ Draw a single point at x,y """ if __debug__: verbose.report("RendererAgg.draw_point", "debug-annoying") rgbFace = gc.get_rgb() self._renderer.draw_ellipse(gc, rgbFace, x, y, 0.5, 0.5, 0.0) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report("RendererAgg.draw_mathtext", "debug-annoying") ox, oy, width, height, descent, font_image, used_characters = self.mathtext_parser.parse( s, self.dpi.get(), prop ) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) if 0: self._renderer.draw_rectangle(gc, None, int(x), self.height - int(y), width, height) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report("RendererAgg.draw_text", "debug-annoying") if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT) else: font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT) font.draw_glyphs_to_bitmap() # print x, y, int(x), int(y) # We pass '0' for angle here, since is has already been rotated # (in vector space) in the above call to font.set_text. self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make cacheing in the # texmanager more efficient. It is not meant to be used # outside the backend """ if ismath == "TeX": # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi.get()) m, n = Z.shape # TODO: descent of TeX text (I am imitating backend_ps here -JKS) return n, m, 0 if ismath: ox, oy, width, height, descent, fonts, used_characters = self.mathtext_parser.parse(s, self.dpi.get(), prop) return width, height, descent font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() dpi = self.dpi.get() texmanager = self.get_texmanager() key = s, size, dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, dpi) Z = npy.array(Z * 255.0, npy.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): "return the canvas width and height in display coords" return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report("RendererAgg._get_agg_font", "debug-annoying") key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi.get()) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report("RendererAgg.points_to_pixels", "debug-annoying") return points * self.dpi.get() / 72.0 def tostring_rgb(self): if __debug__: verbose.report("RendererAgg.tostring_rgb", "debug-annoying") return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report("RendererAgg.tostring_argb", "debug-annoying") return self._renderer.tostring_argb() def buffer_rgba(self, x, y): if __debug__: verbose.report("RendererAgg.buffer_rgba", "debug-annoying") return self._renderer.buffer_rgba(x, y) def clear(self): self._renderer.clear()
class RendererCairo(RendererBase): fontweights = { 100: cairo.FONT_WEIGHT_NORMAL, 200: cairo.FONT_WEIGHT_NORMAL, 300: cairo.FONT_WEIGHT_NORMAL, 400: cairo.FONT_WEIGHT_NORMAL, 500: cairo.FONT_WEIGHT_NORMAL, 600: cairo.FONT_WEIGHT_BOLD, 700: cairo.FONT_WEIGHT_BOLD, 800: cairo.FONT_WEIGHT_BOLD, 900: cairo.FONT_WEIGHT_BOLD, 'ultralight': cairo.FONT_WEIGHT_NORMAL, 'light': cairo.FONT_WEIGHT_NORMAL, 'normal': cairo.FONT_WEIGHT_NORMAL, 'medium': cairo.FONT_WEIGHT_NORMAL, 'regular': cairo.FONT_WEIGHT_NORMAL, 'semibold': cairo.FONT_WEIGHT_BOLD, 'bold': cairo.FONT_WEIGHT_BOLD, 'heavy': cairo.FONT_WEIGHT_BOLD, 'ultrabold': cairo.FONT_WEIGHT_BOLD, 'black': cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic': cairo.FONT_SLANT_ITALIC, 'normal': cairo.FONT_SLANT_NORMAL, 'oblique': cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): self.dpi = dpi self.gc = GraphicsContextCairo(renderer=self) self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) # Although it may appear natural to automatically call # `self.set_width_height(surface.get_width(), surface.get_height())` # here (instead of having the caller do so separately), this would fail # for PDF/PS/SVG surfaces, which have no way to report their extents. def set_width_height(self, width, height): self.width = width self.height = height def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: ctx.save() if len(fill_c) == 3 or alpha_overrides: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod @cbook.deprecated("3.0") def convert_path(ctx, path, transform, clip=None): _append_path(ctx, path, transform, clip) def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx # Clip the path to the actual rendering extents if it isn't filled. clip = (ctx.clip_extents() if rgbFace is None and gc.get_hatch() is None else None) transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() _append_path(ctx, path, transform, clip) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None): ctx = gc.ctx ctx.new_path() # Create the path for the marker; it needs to be flipped here already! _append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill x1, y1, x2, y2 = ctx.fill_extents() if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0: filled = False # No fill, just unset this (so we don't try to fill it later on) rgbFace = None else: filled = True transform = (transform + Affine2D().scale(1, -1).translate(0, self.height)) ctx.new_path() for i, (vertices, codes) in enumerate( path.iter_segments(transform, simplify=False)): if len(vertices): x, y = vertices[-2:] ctx.save() # Translate and apply path ctx.translate(x, y) ctx.append_path(marker_path) ctx.restore() # Slower code path if there is a fill; we need to draw # the fill and stroke for each marker at the same time. # Also flush out the drawing every once in a while to # prevent the paths from getting way too long. if filled or i % 1000 == 0: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) # Fast path, if there is no fill, draw everything in one step if not filled: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_path_collection(self, gc, main_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path_ids = [] for path, transform in self._iter_collection_raw_paths( main_transform, paths, all_transforms): path_ids.append((path, Affine2D(transform))) reuse_key = None grouped_draw = [] def _draw_paths(): if not grouped_draw: return gc_vars, rgb_fc = reuse_key gc = copy.copy(gc0) # We actually need to call the setters to reset the internal state. vars(gc).update(gc_vars) for k, v in gc_vars.items(): if k == "_linestyle": # Deprecated, no effect. continue try: getattr(gc, "set" + k)(v) except (AttributeError, TypeError) as e: pass gc.ctx.new_path() paths, transforms = zip(*grouped_draw) grouped_draw.clear() _append_paths(gc.ctx, paths, transforms) self._fill_and_stroke(gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha()) for xo, yo, path_id, gc0, rgb_fc in self._iter_collection( gc, main_transform, all_transforms, path_ids, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id transform = (Affine2D(transform.get_matrix()).translate( xo, yo - self.height).scale(1, -1)) # rgb_fc could be a ndarray, for which equality is elementwise. new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None if new_key == reuse_key: grouped_draw.append((path, transform)) else: _draw_paths() grouped_draw.append((path, transform)) reuse_key = new_key _draw_paths() def draw_image(self, gc, x, y, im): im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1]) surface = cairo.ImageSurface.create_for_data(im.ravel().data, cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1] * 4) ctx = gc.ctx y = self.height - y - im.shape[0] ctx.save() ctx.set_source_surface(surface, float(x), float(y)) ctx.paint() ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to(x, y) ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate(np.deg2rad(-angle)) ctx.set_font_size(size) ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate(np.deg2rad(-angle)) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.select_font_face(fontProp.name, self.fontangles[fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) ctx.show_text(s) for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle(ox, oy, w, h) ctx.set_source_rgb(0, 0, 0) ctx.fill_preserve() ctx.restore() def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size(size) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): self.gc.ctx.save() self.gc._alpha = 1 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): return points / 72 * self.dpi
class RendererGDK(RendererBase): fontweights = { 100: pango.WEIGHT_ULTRALIGHT, 200: pango.WEIGHT_LIGHT, 300: pango.WEIGHT_LIGHT, 400: pango.WEIGHT_NORMAL, 500: pango.WEIGHT_NORMAL, 600: pango.WEIGHT_BOLD, 700: pango.WEIGHT_BOLD, 800: pango.WEIGHT_HEAVY, 900: pango.WEIGHT_ULTRABOLD, 'ultralight': pango.WEIGHT_ULTRALIGHT, 'light': pango.WEIGHT_LIGHT, 'normal': pango.WEIGHT_NORMAL, 'medium': pango.WEIGHT_NORMAL, 'semibold': pango.WEIGHT_BOLD, 'bold': pango.WEIGHT_BOLD, 'heavy': pango.WEIGHT_HEAVY, 'ultrabold': pango.WEIGHT_ULTRABOLD, 'black': pango.WEIGHT_ULTRABOLD, } # cache for efficiency, these must be at class, not instance level layoutd = {} # a map from text prop tups to pango layouts rotated = {} # a map from text prop tups to rotated text pixbufs def __init__(self, gtkDA, dpi): # widget gtkDA is used for: # '<widget>.create_pango_layout(s)' # cmap line below) self.gtkDA = gtkDA self.dpi = dpi self._cmap = gtkDA.get_colormap() self.mathtext_parser = MathTextParser("Agg") def set_pixmap(self, pixmap): self.gdkDrawable = pixmap def set_width_height(self, width, height): """w,h is the figure w,h not the pixmap w,h """ self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): transform = transform + Affine2D(). \ scale(1.0, -1.0).translate(0, self.height) polygons = path.to_polygons(transform, self.width, self.height) for polygon in polygons: # draw_polygon won't take an arbitrary sequence -- it must be a list # of tuples polygon = [(int(round(x)), int(round(y))) for x, y in polygon] if rgbFace is not None: saveColor = gc.gdkGC.foreground gc.gdkGC.foreground = gc.rgb_to_gdk_color(rgbFace) self.gdkDrawable.draw_polygon(gc.gdkGC, True, polygon) gc.gdkGC.foreground = saveColor if gc.gdkGC.line_width > 0: self.gdkDrawable.draw_lines(gc.gdkGC, polygon) def draw_image(self, gc, x, y, im): bbox = gc.get_clip_rectangle() if bbox != None: l, b, w, h = bbox.bounds #rectangle = (int(l), self.height-int(b+h), # int(w), int(h)) # set clip rect? im.flipud_out() rows, cols, image_str = im.as_rgba_str() image_array = np.fromstring(image_str, np.uint8) image_array.shape = rows, cols, 4 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=True, bits_per_sample=8, width=cols, height=rows) array = pixbuf_get_pixels_array(pixbuf) array[:, :, :] = image_array gc = self.new_gc() y = self.height - y - rows try: # new in 2.2 # can use None instead of gc.gdkGC, if don't need clipping self.gdkDrawable.draw_pixbuf(gc.gdkGC, pixbuf, 0, 0, int(x), int(y), cols, rows, gdk.RGB_DITHER_NONE, 0, 0) except AttributeError: # deprecated in 2.2 pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0, int(x), int(y), cols, rows, gdk.RGB_DITHER_NONE, 0, 0) # unflip im.flipud_out() def draw_text(self, gc, x, y, s, prop, angle, ismath): x, y = int(x), int(y) if x < 0 or y < 0: # window has shrunk and text is off the edge return if angle not in (0, 90): warnings.warn('backend_gdk: unable to draw text at angles ' + 'other than 0 or 90') elif ismath: self._draw_mathtext(gc, x, y, s, prop, angle) elif angle == 90: self._draw_rotated_text(gc, x, y, s, prop, angle) else: layout, inkRect, logicalRect = self._get_pango_layout(s, prop) l, b, w, h = inkRect if (x + w > self.width or y + h > self.height): return self.gdkDrawable.draw_layout(gc.gdkGC, x, y - h - b, layout) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) if angle == 90: width, height = height, width x -= width y -= height imw = font_image.get_width() imh = font_image.get_height() N = imw * imh # a numpixels by num fonts array Xall = np.zeros((N, 1), np.uint8) image_str = font_image.as_str() Xall[:, 0] = np.fromstring(image_str, np.uint8) # get the max alpha at each pixel Xs = np.amax(Xall, axis=1) # convert it to it's proper shape Xs.shape = imh, imw pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, has_alpha=True, bits_per_sample=8, width=imw, height=imh) array = pixbuf_get_pixels_array(pixbuf) rgb = gc.get_rgb() array[:, :, 0] = int(rgb[0] * 255) array[:, :, 1] = int(rgb[1] * 255) array[:, :, 2] = int(rgb[2] * 255) array[:, :, 3] = Xs try: # new in 2.2 # can use None instead of gc.gdkGC, if don't need clipping self.gdkDrawable.draw_pixbuf(gc.gdkGC, pixbuf, 0, 0, int(x), int(y), imw, imh, gdk.RGB_DITHER_NONE, 0, 0) except AttributeError: # deprecated in 2.2 pixbuf.render_to_drawable(self.gdkDrawable, gc.gdkGC, 0, 0, int(x), int(y), imw, imh, gdk.RGB_DITHER_NONE, 0, 0) def _draw_rotated_text(self, gc, x, y, s, prop, angle): """ Draw the text rotated 90 degrees, other angles are not supported """ # this function (and its called functions) is a bottleneck # Pango 1.6 supports rotated text, but pygtk 2.4.0 does not yet have # wrapper functions # GTK+ 2.6 pixbufs support rotation gdrawable = self.gdkDrawable ggc = gc.gdkGC layout, inkRect, logicalRect = self._get_pango_layout(s, prop) l, b, w, h = inkRect x = int(x - h) y = int(y - w) if (x < 0 or y < 0 or # window has shrunk and text is off the edge x + w > self.width or y + h > self.height): return key = (x, y, s, angle, hash(prop)) imageVert = self.rotated.get(key) if imageVert != None: gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w) return imageBack = gdrawable.get_image(x, y, w, h) imageVert = gdrawable.get_image(x, y, h, w) imageFlip = gtk.gdk.Image(type=gdk.IMAGE_FASTEST, visual=gdrawable.get_visual(), width=w, height=h) if imageFlip == None or imageBack == None or imageVert == None: warnings.warn("Could not renderer vertical text") return imageFlip.set_colormap(self._cmap) for i in range(w): for j in range(h): imageFlip.put_pixel(i, j, imageVert.get_pixel(j, w - i - 1)) gdrawable.draw_image(ggc, imageFlip, 0, 0, x, y, w, h) gdrawable.draw_layout(ggc, x, y - b, layout) imageIn = gdrawable.get_image(x, y, w, h) for i in range(w): for j in range(h): imageVert.put_pixel(j, i, imageIn.get_pixel(w - i - 1, j)) gdrawable.draw_image(ggc, imageBack, 0, 0, x, y, w, h) gdrawable.draw_image(ggc, imageVert, 0, 0, x, y, h, w) self.rotated[key] = imageVert def _get_pango_layout(self, s, prop): """ Create a pango layout instance for Text 's' with properties 'prop'. Return - pango layout (from cache if already exists) Note that pango assumes a logical DPI of 96 Ref: pango/fonts.c/pango_font_description_set_size() manual page """ # problem? - cache gets bigger and bigger, is never cleared out # two (not one) layouts are created for every text item s (then they # are cached) - why? key = self.dpi, s, hash(prop) value = self.layoutd.get(key) if value != None: return value size = prop.get_size_in_points() * self.dpi / 96.0 size = round(size) font_str = '%s, %s %i' % ( prop.get_name(), prop.get_style(), size, ) font = pango.FontDescription(font_str) # later - add fontweight to font_str font.set_weight(self.fontweights[prop.get_weight()]) layout = self.gtkDA.create_pango_layout(s) layout.set_font_description(font) inkRect, logicalRect = layout.get_pixel_extents() self.layoutd[key] = layout, inkRect, logicalRect return layout, inkRect, logicalRect def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent layout, inkRect, logicalRect = self._get_pango_layout(s, prop) l, b, w, h = inkRect ll, lb, lw, lh = logicalRect return w, h + 1, h - lh def new_gc(self): return GraphicsContextGDK(renderer=self) def points_to_pixels(self, points): return points / 72.0 * self.dpi
class TextToPath(object): """ A class that convert a given text to a path using ttf fonts. """ FONT_SCALE = 50. DPI = 72 def __init__(self): """ Initialization """ self.mathtext_parser = MathTextParser('path') self.tex_font_map = None from matplotlib.cbook import maxdict self._ps_fontd = maxdict(50) self._texmanager = None def _get_font(self, prop): """ find a ttf font. """ fname = font_manager.findfont(prop) font = FT2Font(str(fname)) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ccode)) return char_id def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.quote('%s-%d' % (ps_name, ccode)) return char_id def glyph_to_path(self, glyph, currx=0.): """ convert the ft2font glyph to vertices and codes. """ #Mostly copied from backend_svg.py. verts, codes = [], [] for step in glyph.path: if step[0] == 0: # MOVE_TO verts.append((step[1], step[2])) codes.append(Path.MOVETO) elif step[0] == 1: # LINE_TO verts.append((step[1], step[2])) codes.append(Path.LINETO) elif step[0] == 2: # CURVE3 verts.extend([(step[1], step[2]), (step[3], step[4])]) codes.extend([Path.CURVE3, Path.CURVE3]) elif step[0] == 3: # CURVE4 verts.extend([(step[1], step[2]), (step[3], step[4]), (step[5], step[6])]) codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4]) elif step[0] == 4: # ENDPOLY verts.append(( 0, 0, )) codes.append(Path.CLOSEPOLY) verts = [(x + currx, y) for (x, y) in verts] return verts, codes def get_text_path(self, prop, s, ismath=False, usetex=False): """ convert text *s* to path (a tuple of vertices and codes for matplotlib.math.Path). *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. Effective only if usetex == False. """ if usetex == False: if ismath == False: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font( font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext( prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if verts1: verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. cmap = font.get_charmap() lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = (glyph.linearHoriAdvance / 65536.0) char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: glyph_map_new[char_id] = self.glyph_to_path(glyph) currx += (kern / 64.0) xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, rects def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes by parsing it with mathtext. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] currx, curry = 0, 0 for font, fontsize, s, ox, oy in glyphs: ccode = ord(s) char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = self.glyph_to_path(glyph) xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map, myrects def get_texmanager(self): """ return the :class:`matplotlib.texmanager.TexManager` instance """ if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using matplotlib's usetex mode. """ # codes are modstly borrowed from pdf backend. texmanager = self.get_texmanager() if self.tex_font_map is None: self.tex_font_map = dviread.PsfontsMap( dviread.find_tex_file('pdftex.map')) fontsize = prop.get_size_in_points() if hasattr(texmanager, "get_dvi"): # dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE) dvi = dviread.DviFromFileLike(dvifilelike, self.DPI) else: dvifile = texmanager.make_dvi(s, self.FONT_SCALE) dvi = dviread.Dvi(dvifile, self.DPI) page = next(iter(dvi)) dvi.close() if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. #oldfont, seq = None, [] for x1, y1, dvifont, glyph, width in page.text: font_and_encoding = self._ps_fontd.get(dvifont.texname) if font_and_encoding is None: font_bunch = self.tex_font_map[dvifont.texname] font = FT2Font(str(font_bunch.filename)) try: font.select_charmap(1094992451) # select ADOBE_CUSTOM except ValueError: font.set_charmap(0) if font_bunch.encoding: enc = dviread.Encoding(font_bunch.encoding) else: enc = None self._ps_fontd[dvifont.texname] = font, enc else: font, enc = font_and_encoding ft2font_flag = LOAD_TARGET_LIGHT char_id = self._get_char_id_ps(font, glyph) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = self.glyph_to_path(glyph0) glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return zip(glyph_ids, xpositions, ypositions, sizes), \ glyph_map_new, myrects
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug = 1 texd = maxdict(50) # a cache of tex image rasters _fontd = maxdict(50) def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report( 'RendererAgg.__init__ width=%s, height=%s' % (width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') #self.draw_path = self._renderer.draw_path # see below self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.restore_region = self._renderer.restore_region self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = npy.ceil(npts / float(nmax)) chsize = int(npy.ceil(npts / nch)) i0 = npy.arange(0, npts, chsize) i1 = npy.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1, :] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) x = int(x) + ox y = int(y) - oy self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=LOAD_FORCE_AUTOHINT) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=LOAD_FORCE_AUTOHINT) font.draw_glyphs_to_bitmap() #print x, y, int(x), int(y) self._renderer.draw_text_image(font.get_image(), int(x), int(y) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make cacheing in the # texmanager more efficient. It is not meant to be used # outside the backend """ if ismath == 'TeX': # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=LOAD_FORCE_AUTOHINT ) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = npy.array(Z * 255.0, npy.uint8) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') key = hash(prop) font = self._fontd.get(key) if font is None: fname = findfont(prop) font = self._fontd.get(fname) if font is None: font = FT2Font(str(fname)) self._fontd[fname] = font self._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points * self.dpi / 72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self, x, y): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba(x, y) def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True
class RendererCairo(RendererBase): fontweights = { 100: cairo.FONT_WEIGHT_NORMAL, 200: cairo.FONT_WEIGHT_NORMAL, 300: cairo.FONT_WEIGHT_NORMAL, 400: cairo.FONT_WEIGHT_NORMAL, 500: cairo.FONT_WEIGHT_NORMAL, 600: cairo.FONT_WEIGHT_BOLD, 700: cairo.FONT_WEIGHT_BOLD, 800: cairo.FONT_WEIGHT_BOLD, 900: cairo.FONT_WEIGHT_BOLD, 'ultralight': cairo.FONT_WEIGHT_NORMAL, 'light': cairo.FONT_WEIGHT_NORMAL, 'normal': cairo.FONT_WEIGHT_NORMAL, 'medium': cairo.FONT_WEIGHT_NORMAL, 'semibold': cairo.FONT_WEIGHT_BOLD, 'bold': cairo.FONT_WEIGHT_BOLD, 'heavy': cairo.FONT_WEIGHT_BOLD, 'ultrabold': cairo.FONT_WEIGHT_BOLD, 'black': cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic': cairo.FONT_SLANT_ITALIC, 'normal': cairo.FONT_SLANT_NORMAL, 'oblique': cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.dpi = dpi self.gc = GraphicsContextCairo(renderer=self) self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix(yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke(self, ctx, fill_c, alpha): if fill_c is not None: ctx.save() if len(fill_c) == 3: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha * fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod def convert_path(ctx, path, transform): for points, code in path.iter_segments(transform): if code == Path.MOVETO: ctx.move_to(*points) elif code == Path.CLOSEPOLY: ctx.close_path() elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: ctx.curve_to(points[0], points[1], points[0], points[1], points[2], points[3]) elif code == Path.CURVE4: ctx.curve_to(*points) def draw_path(self, gc, path, transform, rgbFace=None): if len(path.vertices) > 18980: raise ValueError( "The Cairo backend can not draw paths longer than 18980 points." ) ctx = gc.ctx transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) ctx.new_path() self.convert_path(ctx, path, transform) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha()) def draw_image(self, gc, x, y, im): # bbox - not currently used if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) im.flipud_out() rows, cols, buf = im.color_conv(BYTE_FORMAT) surface = cairo.ImageSurface.create_for_data(buf, cairo.FORMAT_ARGB32, cols, rows, cols * 4) ctx = gc.ctx y = self.height - y - rows ctx.set_source_surface(surface, x, y) ctx.paint() im.flipud_out() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to(x, y) ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate(-angle * np.pi / 180) ctx.set_font_size(size) if sys.version_info[0] < 3: ctx.show_text(s.encode("utf-8")) else: ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate(-angle * np.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face(fontProp.name, self.fontangles[fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) if sys.version_info[0] < 3: ctx.show_text(s.encode("utf-8")) else: ctx.show_text(s) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle(ox, oy, w, h) ctx.set_source_rgb(0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face(prop.get_name(), self.fontangles[prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size(size) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.gc.ctx.save() self.gc._alpha = 1.0 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return points / 72.0 * self.dpi
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None, image_dpi=72): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self.image_dpi = image_dpi # the actual dpi we want to rasterize stuff with self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = OrderedDict() self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = OrderedDict() self._has_gouraud = False self._n_gradients = 0 self._fonts = OrderedDict() self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', width='%ipt' % width, height='%ipt' % height, viewBox='0 0 %i %i' % (width, height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) self.writer.flush() def _write_default_style(self): writer = self.writer default_style = generate_css({ 'stroke-linejoin': 'round', 'stroke-linecap': 'butt' }) writer.start('defs') writer.start('style', type='text/css') writer.data('*{%s}\n' % default_style) writer.end('style') writer.end('defs') def _make_id(self, type, content): content = str(content) if rcParams['svg.hashsalt'] is None: salt = str(uuid.uuid4()) else: salt = rcParams['svg.hashsalt'] if six.PY3: content = content.encode('utf8') salt = salt.encode('utf8') m = md5() m.update(salt) m.update(content) return '%s%s' % (type, m.hexdigest()[:10]) def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _get_font(self, prop): fname = findfont(prop) font = get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ if rgbFace is not None: rgbFace = tuple(rgbFace) edge = gc.get_hatch_color() if edge is not None: edge = tuple(edge) dictkey = (gc.get_hatch(), rgbFace, edge) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start('defs') for ((path, face, stroke), oid) in six.itervalues(self._hatchd): writer.start('pattern', id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=six.text_type(HATCH_SIZE), height=six.text_type(HATCH_SIZE)) path_data = self._convert_path(path, Affine2D().scale(HATCH_SIZE).scale( 1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if face is None: fill = 'none' else: fill = rgb2hex(face) writer.element('rect', x="0", y="0", width=six.text_type(HATCH_SIZE + 1), height=six.text_type(HATCH_SIZE + 1), fill=fill) writer.element('path', d=path_data, style=generate_css({ 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), 'stroke-width': six.text_type(rcParams['hatch.linewidth']), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' })) writer.end('pattern') writer.end('defs') def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace """ attrib = {} forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace) if rgbFace is not None and len( rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) else: if rgbFace is None: attrib['fill'] = 'none' else: if tuple(rgbFace[:3]) != (0, 0, 0): attrib['fill'] = rgb2hex(rgbFace) if len(rgbFace ) == 4 and rgbFace[3] != 1.0 and not forced_alpha: attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) if forced_alpha and gc.get_alpha() != 1.0: attrib['opacity'] = short_float_fmt(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib['stroke-dasharray'] = ','.join( [short_float_fmt(val) for val in seq]) attrib['stroke-dashoffset'] = short_float_fmt(float(offset)) linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() attrib['stroke'] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: attrib['stroke-opacity'] = short_float_fmt(rgb[3]) if linewidth != 1.0: attrib['stroke-width'] = short_float_fmt(linewidth) if gc.get_joinstyle() != 'round': attrib['stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'butt': attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id('p', dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start('defs') for clip, oid in six.itervalues(self._clipd): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element('path', d=path_data) else: x, y, w, h = clip writer.element('rect', x=short_float_fmt(x), y=short_float_fmt(y), width=short_float_fmt(w), height=short_float_fmt(h)) writer.end('clipPath') writer.end('defs') def _write_svgfonts(self): if not rcParams['svg.fonttype'] == 'svgfont': return writer = self.writer writer.start('defs') for font_fname, chars in six.iteritems(self._fonts): font = get_font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() writer.start('font', id=sfnt[(1, 0, 0, 4)]) writer.element( 'font-face', attrib={ 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'units-per-em': '72', 'bbox': ' '.join(short_float_fmt(x / 64.0) for x in font.bbox) }) for char in chars: glyph = font.load_char(char, flags=LOAD_NO_HINTING) verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) # name = font.get_glyph_name(char) writer.element( 'glyph', d=path_data, attrib={ # 'glyph-name': name, 'unicode': unichr(char), 'horiz-adv-x': short_float_fmt(glyph.linearHoriAdvance / 65536.0) }) writer.end('font') writer.end('defs') def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start('g', id="%s_%d" % (s, self._groupd[s])) def close_group(self, s): self.writer.end('g') def option_image_nocomposite(self): """ return whether to generate a composite image from multiple images on a set of axes """ return not rcParams['image.composite_image'] def _convert_path(self, path, transform=None, clip=None, simplify=None, sketch=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_string(path, transform, clip, simplify, sketch, 6, [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify, sketch=gc.get_sketch_params()) attrib = {} attrib['style'] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) self.writer.element('path', d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end('a') def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if not len(path.vertices): return writer = self.writer path_data = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) for key in list(six.iterkeys(style)): if not key.startswith('stroke'): del style[key] style = generate_css(style) if oid is None: oid = self._make_id('m', dictkey) writer.start('defs') writer.element('path', id=oid, d=path_data, style=style) writer.end('defs') self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid writer.start('g', attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': '#%s' % oid} clip = (0, 0, self.width * 72, self.height * 72) for vertices, code in path.iter_segments(trans_and_flip, clip=clip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib['x'] = short_float_fmt(x) attrib['y'] = short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 5) * uses_per_path # cost of definition+use is # (len_path + 3) + 9 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) should_do_optimization = \ len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) writer = self.writer path_codes = [] writer.start('defs') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = 'C%x_%x_%s' % (self._path_collection_id, i, self._make_id('', d)) writer.element('path', id=oid, d=d) path_codes.append(oid) writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) if clipid is not None: writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) attrib = { 'xlink:href': '#%s' % path_id, 'x': short_float_fmt(xo), 'y': short_float_fmt(self.height - yo), 'style': self._get_style(gc0, rgbFace) } writer.element('use', attrib=attrib) if clipid is not None: writer.end('g') if url is not None: writer.end('a') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start('filter', id='colorAdd') writer.element('feComposite', attrib={'in': 'SourceGraphic'}, in2='BackgroundImage', operator='arithmetic', k2="1", k3="1") writer.end('filter') avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start('defs') for i in range(3): x1, y1 = tpoints[i] x2, y2 = tpoints[(i + 1) % 3] x3, y3 = tpoints[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start('linearGradient', id="GR%x_%d" % (self._n_gradients, i), x1=short_float_fmt(x1), y1=short_float_fmt(y1), x2=short_float_fmt(xb), y2=short_float_fmt(yb)) writer.element('stop', offset='0', style=generate_css({ 'stop-color': rgb2hex(c), 'stop-opacity': short_float_fmt(c[-1]) })) writer.element('stop', offset='1', style=generate_css({ 'stop-color': rgb2hex(c), 'stop-opacity': "0" })) writer.end('linearGradient') writer.element('polygon', id='GT%x' % self._n_gradients, points=" ".join([ short_float_fmt(x) for x in (x1, y1, x2, y2, x3, y3) ])) writer.end('defs') avg_color = np.sum(colors[:, :], axis=0) / 3.0 href = '#GT%x' % self._n_gradients writer.element('use', attrib={ 'xlink:href': href, 'fill': rgb2hex(avg_color), 'fill-opacity': short_float_fmt(avg_color[-1]) }) for i in range(3): writer.element('use', attrib={ 'xlink:href': href, 'fill': 'url(#GR%x_%d)' % (self._n_gradients, i), 'fill-opacity': '1', 'filter': 'url(#colorAdd)' }) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid self.writer.start('g', attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end('g') def option_scale_image(self): return True def get_image_magnification(self): return self.image_dpi / 72.0 def draw_image(self, gc, x, y, im, transform=None): h, w = im.shape[:2] if w == 0 or h == 0: return attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if rcParams['svg.image_inline']: bytesio = io.BytesIO() _png.write_png(im, bytesio) oid = oid or self._make_id('image', bytesio.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode('ascii')) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) _png.write_png(im, filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename attrib['id'] = oid if transform is None: w = 72.0 * w / self.image_dpi h = 72.0 * h / self.image_dpi self.writer.element('image', transform=generate_transform([ ('scale', (1, -1)), ('translate', (0, -h)) ]), x=short_float_fmt(x), y=short_float_fmt(-(self.height - y - h)), width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = short_float_fmt(alpha) flipped = (Affine2D().scale(1.0 / w, 1.0 / h) + transform + Affine2D().translate(x, y).scale(1.0, -1.0).translate( 0.0, self.height)) attrib['transform'] = generate_transform([('matrix', flipped.frozen())]) self.writer.element('image', width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = short_float_fmt(gc.get_alpha()) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs if glyph_map_new: writer.start('defs') for char_id, glyph_path in six.iteritems(glyph_map_new): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} attrib['style'] = generate_css(style) font_scale = fontsize / text2path.FONT_SCALE attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle, )), ('scale', (font_scale, -font_scale)) ]) writer.start('g', attrib=attrib) for glyph_id, xposition, yposition, scale in glyph_info: attrib = {'xlink:href': '#%s' % glyph_id} if xposition != 0.0: attrib['x'] = short_float_fmt(xposition) if yposition != 0.0: attrib['y'] = short_float_fmt(yposition) writer.element('use', attrib=attrib) writer.end('g') else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext( prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: writer.start('defs') for char_id, glyph_path in six.iteritems(glyph_map_new): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): path_data = "" else: path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} font_scale = fontsize / text2path.FONT_SCALE attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle, )), ('scale', (font_scale, -font_scale)) ]) writer.start('g', attrib=attrib) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element('use', transform=generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale, )), ]), attrib={'xlink:href': '#%s' % char_id}) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element('path', d=path_data) writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = short_float_fmt(gc.get_alpha()) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) fontsize = prop.get_size_in_points() fontfamily = font.family_name fontstyle = prop.get_style() attrib = {} # Must add "px" to workaround a Firefox bug style['font-size'] = short_float_fmt(fontsize) + 'px' style['font-family'] = six.text_type(fontfamily) style['font-style'] = prop.get_style().lower() style['font-weight'] = six.text_type(prop.get_weight()).lower() attrib['style'] = generate_css(style) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original # coordinates and add alignment information. # Get anchor coordinates. transform = mtext.get_transform() ax, ay = transform.transform_point(mtext.get_position()) ay = self.height - ay # Don't do vertical anchor alignment. Most applications do not # support 'alignment-baseline' yet. Apply the vertical layout # to the anchor point manually for now. angle_rad = angle * np.pi / 180. dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] ha_mpl_to_svg = { 'left': 'start', 'right': 'end', 'center': 'middle' } style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] attrib['x'] = short_float_fmt(ax) attrib['y'] = short_float_fmt(ay) attrib['style'] = generate_css(style) attrib['transform'] = "rotate(%s, %s, %s)" % (short_float_fmt( -angle), short_float_fmt(ax), short_float_fmt(ay)) writer.element('text', s, attrib=attrib) else: attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle, )) ]) writer.element('text', s, attrib=attrib) if rcParams['svg.fonttype'] == 'svgfont': fontset = self._fonts.setdefault(font.fname, set()) for c in s: fontset.add(ord(c)) else: writer.comment(s) width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([('translate', (x, y)), ('rotate', (-angle, ))]) # Apply attributes to 'g', not 'text', because we likely # have some rectangles as well with the same style and # transformation writer.start('g', attrib=attrib) writer.start('text') # Sort the characters by font, and output one tspan for # each spans = OrderedDict() for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css({ 'font-size': short_float_fmt(fontsize) + 'px', 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'font-weight': font.style_name.lower() }) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) if rcParams['svg.fonttype'] == 'svgfont': for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) for style, chars in six.iteritems(spans): chars.sort() same_y = True if len(chars) > 1: last_y = chars[0][1] for i in xrange(1, len(chars)): if chars[i][1] != last_y: same_y = False break if same_y: ys = six.text_type(chars[0][1]) else: ys = ' '.join(six.text_type(c[1]) for c in chars) attrib = { 'style': style, 'x': ' '.join(short_float_fmt(c[0]) for c in chars), 'y': ys } writer.element('tspan', ''.join(unichr(c[2]) for c in chars), attrib=attrib) writer.end('text') if len(svg_rects): for x, y, width, height in svg_rects: writer.element('rect', x=short_float_fmt(x), y=short_float_fmt(-y + height), width=short_float_fmt(width), height=short_float_fmt(height)) writer.end('g') def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) if rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): return self._text2path.get_text_width_height_descent(s, prop, ismath)
class RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forward the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() self.gc.set_dpi(self.dpi) self.mathtext_parser = MathTextParser('MacOSX') def set_width_height(self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace[:3]) linewidth = gc.get_linewidth() gc.draw_path(path, transform, linewidth, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace[:3]) linewidth = gc.get_linewidth() gc.draw_markers(marker_path, marker_trans, path, trans, linewidth, rgbFace) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): if offset_position == 'data': offset_position = True else: offset_position = False path_ids = [] for path, transform in self._iter_collection_raw_paths( master_transform, paths, all_transforms): path_ids.append((path, transform)) master_transform = master_transform.get_matrix() all_transforms = [t.get_matrix() for t in all_transforms] offsetTrans = offsetTrans.get_matrix() gc.draw_path_collection(master_transform, path_ids, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, offset_position) def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, edgecolors): gc.draw_quad_mesh(master_transform.get_matrix(), meshWidth, meshHeight, coordinates, offsets, offsetTrans.get_matrix(), facecolors, antialiased, edgecolors) def new_gc(self): self.gc.save() self.gc.set_hatch(None) self.gc._alpha = 1.0 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def draw_gouraud_triangle(self, gc, points, colors, transform): points = transform.transform(points) gc.draw_gouraud_triangle(points, colors) def draw_image(self, gc, x, y, im): im.flipud_out() nrows, ncols, data = im.as_rgba_str() gc.draw_image(x, y, nrows, ncols, data, gc.get_clip_rectangle(), *gc.get_clip_path()) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get( key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) gc.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) width, height, descent = self.gc.get_text_width_height_descent( unicode(s), family, size, weight, style) return width, height, 0.0 * descent def flipy(self): return False def points_to_pixels(self, points): return points / 72.0 * self.dpi def option_image_nocomposite(self): return True
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.dpi = dpi self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) self.mathtext_parser = MathTextParser('Cairo') def set_ctx_from_surface (self, surface): self.ctx = cairo.Context (surface) self.ctx.save() # restore, save - when call new_gc() def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke (self, ctx, fill_c): #assert fill_c or stroke_c #_.ctx.save() if fill_c: ctx.save() ctx.set_source_rgb (*fill_c) #if stroke_c: # always (implicitly) set at the moment ctx.fill_preserve() #else: # ctx.fill() ctx.restore() #if stroke_c: # always stroke #ctx.set_source_rgb (stroke_c) # is already set ctx.stroke() #_.ctx.restore() # revert to the default attributes def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx ctx.save() ctx.translate(x, self.height - y) ctx.rotate(rotation) ctx.scale(width / 2.0, height / 2.0) ctx.new_sub_path() ctx.arc(0.0, 0.0, 1.0, npy.pi * angle1 / 180., npy.pi * angle2 / 180.) ctx.restore() self._fill_and_stroke (ctx, rgbFace) def draw_image(self, x, y, im, bbox): # bbox - not currently used if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) im.flipud_out() rows, cols, buf = im.color_conv (BYTE_FORMAT) surface = cairo.ImageSurface.create_for_data ( buf, cairo.FORMAT_ARGB32, cols, rows, cols*4) # function does not pass a 'gc' so use renderer.ctx ctx = self.ctx y = self.height - y - rows ctx.set_source_surface (surface, x, y) ctx.paint() im.flipud_out() def draw_line(self, gc, x1, y1, x2, y2): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx ctx.new_path() ctx.move_to (x1, self.height - y1) ctx.line_to (x2, self.height - y2) self._fill_and_stroke (ctx, None) def draw_lines(self, gc, x, y, transform=None): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if transform: if transform.need_nonlinear(): x, y = transform.nonlinear_only_numerix(x, y) x, y = transform.numerix_x_y(x, y) ctx = gc.ctx matrix_old = ctx.get_matrix() ctx.set_matrix (self.matrix_flipy) points = izip(x,y) x, y = points.next() ctx.new_path() ctx.move_to (x, y) for x,y in points: ctx.line_to (x, y) self._fill_and_stroke (ctx, None) ctx.set_matrix (matrix_old) def draw_markers_OLD(self, gc, path, rgbFace, x, y, transform): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx if transform.need_nonlinear(): x,y = transform.nonlinear_only_numerix(x, y) x, y = transform.numerix_x_y(x, y) # do nonlinear and affine transform # TODO - use cairo transform # matrix worked for dotted lines, but not markers in line_styles.py # it upsets/transforms generate_path() ? # need to flip y too, and update generate_path() ? # the a,b,c,d,tx,ty affine which transforms x and y #vec6 = transform.as_vec6_val() # not used (yet) #matrix_old = ctx.get_matrix() #ctx.set_matrix (cairo.Matrix (*vec6)) path_list = [path.vertex() for i in range(path.total_vertices())] def generate_path (path_list): for code, xp, yp in path_list: if code == agg.path_cmd_move_to: ctx.move_to (xp, -yp) elif code == agg.path_cmd_line_to: ctx.line_to (xp, -yp) elif code == agg.path_cmd_end_poly: ctx.close_path() for x,y in izip(x,y): ctx.save() ctx.new_path() ctx.translate(x, self.height - y) generate_path (path_list) self._fill_and_stroke (ctx, rgbFace) ctx.restore() # undo translate() #ctx.set_matrix(matrix_old) def draw_point(self, gc, x, y): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) # render by drawing a 0.5 radius circle ctx = gc.ctx ctx.new_path() ctx.arc (x, self.height - y, 0.5, 0, 2*npy.pi) self._fill_and_stroke (ctx, gc.get_rgb()) def draw_polygon(self, gc, rgbFace, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx matrix_old = ctx.get_matrix() ctx.set_matrix (self.matrix_flipy) ctx.new_path() x, y = points[0] ctx.move_to (x, y) for x,y in points[1:]: ctx.line_to (x, y) ctx.close_path() self._fill_and_stroke (ctx, rgbFace) ctx.set_matrix (matrix_old) def draw_rectangle(self, gc, rgbFace, x, y, width, height): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx ctx.new_path() ctx.rectangle (x, self.height - y - height, width, height) self._fill_and_stroke (ctx, rgbFace) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # size = prop.get_size_in_points() * self.dpi.get() / 96.0 size = prop.get_size_in_points() * self.dpi.get() / 72.0 ctx.save() if angle: ctx.rotate (-angle * npy.pi / 180) ctx.set_font_size (size) ctx.show_text (s.encode("utf-8")) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi.get(), prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate (-angle * npy.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) # size = prop.get_size_in_points() * self.dpi.get() / 96.0 size = fontsize * self.dpi.get() / 72.0 ctx.set_font_size(size) ctx.show_text(s.encode("utf-8")) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle (ox, oy, w, h) ctx.set_source_rgb (0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi.get(), prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small #size = prop.get_size_in_points() * self.dpi.get() / 96.0 size = prop.get_size_in_points() * self.dpi.get() / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size (size) y_bearing, w, h = ctx.text_extents (s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) self.ctx.restore() # matches save() in set_ctx_from_surface() self.ctx.save() return GraphicsContextCairo (renderer=self) def points_to_pixels(self, points): if _debug: print '%s.%s()' % (self.__class__.__name__, _fn_name()) return points/72.0 * self.dpi.get()
class TextToPath: """A class that converts strings to paths.""" FONT_SCALE = 100. DPI = 72 def __init__(self): self.mathtext_parser = MathTextParser('path') self._texmanager = None def _get_font(self, prop): """ Find the `FT2Font` matching font properties *prop*, with its size set. """ fname = font_manager.findfont(prop) font = get_font(fname) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode)) def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode)) return char_id @cbook.deprecated( "3.1", alternative="font.get_path() and manual translation of the vertices") def glyph_to_path(self, font, currx=0.): """Convert the *font*'s current glyph to a (vertices, codes) pair.""" verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = fontsize / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale @cbook._delete_parameter("3.1", "usetex") def get_text_path(self, prop, s, ismath=False, usetex=False): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). Parameters ---------- prop : `matplotlib.font_manager.FontProperties` instance The font properties for the text. s : str The text to be converted. ismath : {False, True, "TeX"} If True, use mathtext parser. If "TeX", use tex for renderering. usetex : bool, optional If set, forces *ismath* to True. This parameter is deprecated. Returns ------- verts, codes : tuple of lists *verts* is a list of numpy arrays containing the x and y coordinates of the vertices. *codes* is a list of path codes. Examples -------- Create a list of vertices and codes from a text, and create a `Path` from those:: from matplotlib.path import Path from matplotlib.textpath import TextToPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Humor Sans", style="italic") verts, codes = TextToPath().get_text_path(fp, "ABC") path = Path(verts, codes, closed=False) Also see `TextPath` for a more direct way to create a path from a text. """ if usetex: ismath = "TeX" if ismath == "TeX": glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ Convert string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = glyph.linearHoriAdvance / 65536 char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: glyph_map_new[char_id] = font.get_path() currx += kern / 64 xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Parse mathtext string *s* and convert it to a (vertices, codes) pair. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """Convert the string *s* to vertices and codes using usetex mode.""" # Mostly borrowed from pdf backend. dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) with dviread.Dvi(dvifile, self.DPI) as dvi: page, = dvi if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. for x1, y1, dvifont, glyph, width in page.text: font, enc = self._get_ps_font_and_encoding(dvifont.texname) char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) # See comments in _get_ps_font_and_encoding. if enc is not None: index = font.get_name_index(enc[glyph]) font.load_glyph(index, flags=LOAD_TARGET_LIGHT) else: font.load_char(glyph, flags=LOAD_TARGET_LIGHT) glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) font_bunch = tex_font_map[texname] if font_bunch.filename is None: raise ValueError( f"No usable font file found for {font_bunch.psname} " f"({texname}). The font may lack a Type-1 version.") font = get_font(font_bunch.filename) if font_bunch.encoding: # If psfonts.map specifies an encoding, use it: it gives us a # mapping of glyph indices to Adobe glyph names; use it to convert # dvi indices to glyph names and use the FreeType-synthesized # unicode charmap to convert glyph names to glyph indices (with # FT_Get_Name_Index/get_name_index), and load the glyph using # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least # as good as, and possibly better than, the native charmaps.) enc = dviread._parse_enc(font_bunch.encoding) else: # If psfonts.map specifies no encoding, the indices directly # map to the font's "native" charmap; so don't use the # FreeType-synthesized charmap but the native ones (we can't # directly identify it but it's typically an Adobe charmap), and # directly load the dvi glyph indices using FT_Load_Char/load_char. for charmap_code in [ 1094992451, # ADOBE_CUSTOM. 1094995778, # ADOBE_STANDARD. ]: try: font.select_charmap(charmap_code) except (ValueError, RuntimeError): pass else: break else: _log.warning("No supported encoding in font (%s).", font_bunch.filename) enc = None return font, enc
class RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forwards the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() self.mathtext_parser = MathTextParser('MacOSX') def set_width_height (self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): path = transform.transform_path(path) for points, code in path.iter_segments(): if code == Path.MOVETO: gc.moveto(points) elif code == Path.LINETO: gc.lineto(points) elif code == Path.CURVE3: gc.curve3(points) elif code == Path.CURVE4: gc.curve4(points) elif code == Path.CLOSEPOLY: gc.closepoly() if rgbFace is not None: rgbFace = tuple(rgbFace) gc.stroke(rgbFace) def new_gc(self): self.gc.reset() return self.gc def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): self.gc.set_clip_rectangle(bbox) im.flipud_out() nrows, ncols, data = im.as_rgba_str() self.gc.draw_image(x, y, nrows, ncols, data) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): size = prop.get_size_in_points() ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) gc.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() size = prop.get_size_in_points() weight = prop.get_weight() style = prop.get_style() gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath=='TeX': # TODO: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) m,n = Z.shape # TODO: handle descent; This is based on backend_agg.py return n, m, 0 if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent family = prop.get_family() size = prop.get_size_in_points() weight = prop.get_weight() style = prop.get_style() return self.gc.get_text_width_height_descent(unicode(s), family, size, weight, style) def flipy(self): return False def points_to_pixels(self, points): return points/72.0 * self.dpi def option_image_nocomposite(self): return True
class RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forwards the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() self.mathtext_parser = MathTextParser('MacOSX') def set_width_height (self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) gc.draw_path(path, transform, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) gc.draw_markers(marker_path, marker_trans, path, trans, rgbFace) def draw_path_collection(self, *args): gc = self.gc args = args[:13] gc.draw_path_collection(*args) def draw_quad_mesh(self, *args): gc = self.gc gc.draw_quad_mesh(*args) def new_gc(self): self.gc.save() self.gc.set_hatch(None) return self.gc def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): im.flipud_out() nrows, ncols, data = im.as_rgba_str() self.gc.draw_image(x, y, nrows, ncols, data, bbox, clippath, clippath_trans) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) gc.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath=='TeX': # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) width, height, descent = self.gc.get_text_width_height_descent(unicode(s), family, size, weight, style) return width, height, 0.0*descent def flipy(self): return False def points_to_pixels(self, points): return points/72.0 * self.dpi def option_image_nocomposite(self): return True
class RendererGR(RendererBase): """ Handles drawing/rendering operations using GR """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): self.dpi = dpi self.width = float(width) * dpi / 80 self.height = float(height) * dpi / 80 self.mathtext_parser = MathTextParser('agg') self.texmanager = TexManager() def configure(self): aspect_ratio = self.width / self.height if aspect_ratio > 1: rect = np.array([0, 1, 0, 1.0 / aspect_ratio]) self.size = self.width else: rect = np.array([0, aspect_ratio, 0, 1]) self.size = self.height mwidth, mheight, width, height = gr.inqdspsize() if width / (mwidth / 0.0256) < 200: mwidth *= self.width / width gr.setwsviewport(*rect * mwidth) else: gr.setwsviewport(*rect * 0.192) gr.setwswindow(*rect) gr.setviewport(*rect) gr.setwindow(0, self.width, 0, self.height) def draw_path(self, gc, path, transform, rgbFace=None): path = transform.transform_path(path) points = path.vertices codes = path.codes bbox = gc.get_clip_rectangle() if bbox is not None: x, y, w, h = bbox.bounds clrt = np.array([x, x + w, y, y + h]) else: clrt = np.array([0, self.width, 0, self.height]) gr.setviewport(*clrt / self.size) gr.setwindow(*clrt) if rgbFace is not None and len(points) > 2: color = gr.inqcolorfromrgb(rgbFace[0], rgbFace[1], rgbFace[2]) gr.settransparency(rgbFace[3]) gr.setcolorrep(color, rgbFace[0], rgbFace[1], rgbFace[2]) gr.setfillintstyle(gr.INTSTYLE_SOLID) gr.setfillcolorind(color) gr.drawpath(points, codes, fill=True) lw = gc.get_linewidth() if lw != 0: rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) if isinstance(gc._linestyle, str): gr.setlinetype(linetype[gc._linestyle]) gr.setlinewidth(lw) gr.setlinecolorind(color) gr.drawpath(points, codes, fill=False) def draw_image(self, gc, x, y, im): h, w, s = im.as_rgba_str() img = np.fromstring(s, np.uint32) img.shape = (h, w) gr.drawimage(x, x + w, y + h, y, w, h, img) def draw_mathtext(self, x, y, angle, Z): h, w = Z.shape img = np.zeros((h, w), np.uint32) for i in range(h): for j in range(w): img[i, j] = (255 - Z[i, j]) << 24 a = int(angle) if a == 90: gr.drawimage(x - h, x, y, y + w, h, w, np.resize(np.rot90(img, 1), (h, w))) elif a == 180: gr.drawimage(x - w, x, y - h, y, w, h, np.rot90(img, 2)) elif a == 270: gr.drawimage(x, x + h, y - w, y, h, w, np.resize(np.rot90(img, 3), (h, w))) else: gr.drawimage(x, x + w, y, y + h, w, h, img) def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): size = prop.get_size_in_points() key = s, size, self.dpi, angle, self.texmanager.get_font_config() im = self.texd.get(key) if im is None: Z = self.texmanager.get_grey(s, size, self.dpi) Z = np.array(255.0 - Z * 255.0, np.uint8) self.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) self.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: x, y = gr.wctondc(x, y) s = s.replace(u'\u2212', '-') fontsize = prop.get_size_in_points() rgba = gc.get_rgb()[:4] color = gr.inqcolorfromrgb(rgba[0], rgba[1], rgba[2]) gr.settransparency(rgba[3]) gr.setcolorrep(color, rgba[0], rgba[1], rgba[2]) gr.setcharheight(fontsize * 0.0013) gr.settextcolorind(color) if angle != 0: gr.setcharup(-np.sin(angle * np.pi/180), np.cos(angle * np.pi/180)) else: gr.setcharup(0, 1) gr.text(x, y, s) def flipy(self): return False def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': fontsize = prop.get_size_in_points() w, h, d = self.texmanager.get_text_width_height_descent( s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent # family = prop.get_family() # weight = prop.get_weight() # style = prop.get_style() fontsize = prop.get_size_in_points() gr.setcharheight(fontsize * 0.0013) gr.setcharup(0, 1) (tbx, tby) = gr.inqtextext(0, 0, s) width, height, descent = tbx[1], tby[2], 0 return width, height, descent def new_gc(self): return GraphicsContextGR() def points_to_pixels(self, points): return points
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 RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ # we want to cache the fonts at the class level so that when # multiple figures are created we can reuse them. This helps with # a bug on windows where the creation of too many figures leads to # too many open file handles. However, storing them at the class # level is not thread safe. The solution here is to let the # FigureCanvas acquire a lock on the fontd at the start of the # draw, and release it when it is done. This allows multiple # renderers to share the cached fonts, but only one figure can # draw at time and so the font cache is used by only one # renderer at a time. lock = threading.RLock() def __init__(self, width, height, dpi): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self._renderer = _RendererAgg(int(width), int(height), dpi) self._filter_renderers = [] self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) def __getstate__(self): # We only want to preserve the init keywords of the Renderer. # Anything else can be re-created. return {'width': self.width, 'height': self.height, 'dpi': self.dpi} def __setstate__(self, state): self.__init__(state['width'], state['height'], state['dpi']) def _update_methods(self): self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.draw_markers = self._renderer.draw_markers # This is its own method for the duration of the deprecation of # offset_position = "data". # self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.copy_from_bbox = self._renderer.copy_from_bbox @cbook.deprecated("3.4") def get_content_extents(self): orig_img = np.asarray(self.buffer_rgba()) slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3]) return (slice_x.start, slice_y.start, slice_x.stop - slice_x.start, slice_y.stop - slice_y.start) @cbook.deprecated("3.4") def tostring_rgba_minimized(self): extents = self.get_content_extents() bbox = [[extents[0], self.height - (extents[1] + extents[3])], [extents[0] + extents[2], self.height - extents[1]]] region = self.copy_from_bbox(bbox) return np.array(region), extents def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (npts > nmax > 100 and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = np.ceil(npts / nmax) chsize = int(np.ceil(npts / nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1, :] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) try: self._renderer.draw_path(gc, p, transform, rgbFace) except OverflowError as err: raise OverflowError( "Exceeded cell block limit (set 'agg.path.chunksize' " "rcparam)") from err else: try: self._renderer.draw_path(gc, path, transform, rgbFace) except OverflowError as err: raise OverflowError("Exceeded cell block limit (set " "'agg.path.chunksize' rcparam)") from err def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): if offset_position == "data": cbook.warn_deprecated( "3.3", message="Support for offset_position='data' is " "deprecated since %(since)s and will be removed %(removal)s.") return self._renderer.draw_path_collection(gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw mathtext using :mod:`matplotlib.mathtext`.""" ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) xd = descent * sin(radians(angle)) yd = descent * cos(radians(angle)) x = round(x + ox + xd) y = round(y - oy + yd) self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap( antialiased=mpl.rcParams['text.antialiased']) d = font.get_descent() / 64.0 # The descent needs to be adjusted for the angle. xo, yo = font.get_bitmap_offset() xo /= 64.0 yo /= 64.0 xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = round(x + xo + xd) y = round(y + yo + yd) self._renderer.draw_text_image(font, x, y + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath in ["TeX", "TeX!"]: if ismath == "TeX!": cbook._warn_deprecated( "3.3", message="Support for ismath='TeX!' is deprecated " "since %(since)s and will be removed %(removal)s; use " "ismath='TeX' instead.") # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) w, h = font.get_width_height() # width and height of unrotated string d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d @cbook._delete_parameter("3.2", "ismath") def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # docstring inherited # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = round(x + xd) y = round(y + yd) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): # docstring inherited return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, caching for efficiency """ fname = findfont(prop) font = get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): # docstring inherited return points * self.dpi / 72 def buffer_rgba(self): return memoryview(self._renderer) def tostring_argb(self): return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes() def tostring_rgb(self): return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes() def clear(self): self._renderer.clear() def option_image_nocomposite(self): # docstring inherited # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): # docstring inherited return False def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a pair of floats) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), ... xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy # The incoming data is float, but the _renderer type-checking wants # to see integers. self._renderer.restore_region(region, int(x1), int(y1), int(x2), int(y2), int(ox), int(oy)) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ orig_img = np.asarray(self.buffer_rgba()) slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3]) cropped_img = orig_img[slice_y, slice_x] self._renderer = self._filter_renderers.pop() self._update_methods() if cropped_img.size: img, ox, oy = post_processing(cropped_img / 255, self.dpi) gc = self.new_gc() if img.dtype.kind == 'f': img = np.asarray(img * 255., np.uint8) self._renderer.draw_image(gc, slice_x.start + ox, int(self.height) - slice_y.stop + oy, img[::-1])
class RendererSVG(RendererBase): FONT_SCALE = 100.0 def __init__(self, width, height, svgwriter, basename=None): self.width=width self.height=height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self.mathtext_parser = MathTextParser('SVG') self.fontd = {} svgwriter.write(svgProlog%(width,height,width,height)) def _draw_svg_element(self, element, details, gc, rgbFace): cliprect, clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid style = self._get_style(gc, rgbFace) self._svgwriter.write ('%s<%s style="%s" %s %s/>\n' % ( cliprect, element, style, clippath, details)) def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = FT2Font(str(fname)) self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %s;' % ( ','.join(['%s'%val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %s; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %s' % ( fill, rgb2hex(gc.get_rgb()), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %s' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() if cliprect is None: return '', None else: # See if we've already seen this clip rectangle key = hash(cliprect) if self._clipd.get(key) is None: # If not, store a new clipPath self._clipd[key] = cliprect x, y, w, h = cliprect y = self.height-(y+h) style = "stroke: gray; fill: none;" box = """\ <defs> <clipPath id="%(key)s"> <rect x="%(x)s" y="%(y)s" width="%(w)s" height="%(h)s" style="%(style)s"/> </clipPath> </defs> """ % locals() return box, key else: # return id of previously defined clipPath return '', key def open_group(self, s): self._groupd[s] = self._groupd.get(s,0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation): """ Ignores angles for now """ details = 'cx="%s" cy="%s" rx="%s" ry="%s" transform="rotate(%1.1f %s %s)"' % \ (x, self.height-y, width/2.0, height/2.0, -rotation, x, self.height-y) self._draw_svg_element('ellipse', details, gc, rgbFace) def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] def draw_image(self, x, y, im, bbox): trans = [1,0,0,1,0,0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) if im.get_interpolation() != 0: trans[4] += trans[0] trans[5] += trans[3] trans[5] = -trans[5] transstr = 'transform="matrix(%s %s %s %s %s %s)" '%tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows,numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h,w = im.get_size_out() if rcParams['svg.image_inline']: filename = os.path.join (tempfile.gettempdir(), tempfile.gettempprefix() + '.png' ) verbose.report ('Writing temporary image file for inlining: %s' % filename) # im.write_png() accepts a filename, not file object, would be # good to avoid using files and write to mem with StringIO # JDH: it *would* be good, but I don't know how to do this # since libpng seems to want a FILE* and StringIO doesn't seem # to provide one. I suspect there is a way, but I don't know # it im.flipud_out() im.write_png(filename) im.flipud_out() imfile = file (filename, 'rb') image64 = base64.encodestring (imfile.read()) imfile.close() os.remove(filename) hrefstr = 'data:image/png;base64,\n' + image64 else: self._imaged[self.basename] = self._imaged.get(self.basename,0) + 1 filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename]) verbose.report( 'Writing image file for inclusion: %s' % filename) im.flipud_out() im.write_png(filename) im.flipud_out() hrefstr = filename self._svgwriter.write ( '<image x="%s" y="%s" width="%s" height="%s" ' 'xlink:href="%s" %s/>\n'%(x/trans[0], (self.height-y)/trans[3]-h, w, h, hrefstr, transstr) ) def draw_line(self, gc, x1, y1, x2, y2): details = 'd="M%s,%sL%s,%s"' % (x1, self.height-y1, x2, self.height-y2) self._draw_svg_element('path', details, gc, None) def draw_lines(self, gc, x, y, transform=None): if len(x)==0: return if len(x)!=len(y): raise ValueError('x and y must be the same length') y = self.height - y details = ['d="M%s,%s' % (x[0], y[0])] xys = zip(x[1:], y[1:]) details.extend(['L%s,%s' % tup for tup in xys]) details.append('"') details = ''.join(details) self._draw_svg_element('path', details, gc, None) def draw_point(self, gc, x, y): # result seems to have a hole in it... self.draw_arc(gc, gc.get_rgb(), x, y, 1, 0, 0, 0, 0) def draw_polygon(self, gc, rgbFace, points): details = 'points = "%s"' % ' '.join(['%s,%s'%(x,self.height-y) for x, y in points]) self._draw_svg_element('polygon', details, gc, rgbFace) def draw_rectangle(self, gc, rgbFace, x, y, width, height): details = 'width="%s" height="%s" x="%s" y="%s"' % (width, height, x, self.height-y-height) self._draw_svg_element('rect', details, gc, rgbFace) def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()) if rcParams['svg.embed_char_paths']: svg = ['<g style="fill: %s" transform="' % color] if angle != 0: svg.append('translate(%s,%s)rotate(%1.1f)' % (x,y,-angle)) elif x != 0 or y != 0: svg.append('translate(%s,%s)' % (x, y)) svg.append('scale(%s)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charid = self._add_char_def(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind currx += kern/64.0 svg.append('<use xlink:href="#%s"' % charid) if currx != 0: svg.append(' transform="translate(%s)"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = 'font-size: %f; font-family: %s; font-style: %s; fill: %s;'%(fontsize, fontfamily,fontstyle, color) if angle!=0: transform = 'transform="translate(%s,%s) rotate(%1.1f) translate(%s,%s)"' % (x,y,-angle,-x,-y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)s" y="%(y)s" %(transform)s>%(thetext)s</text> """ % locals() self._svgwriter.write (svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num, path = self._char_defs.get(char_id, (None, None)) if char_num is not None: return char_num path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%s %s" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%s %s" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%s %s %s %s" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append("c%s %s %s %s %s %s" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] char_num = 'c_%x' % len(self._char_defs) path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = (char_num, path_element) return char_num def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()) self.open_group("mathtext") style = "fill: %s" % color if rcParams['svg.embed_char_paths']: svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%s,%s)rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%s,%s)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._add_char_def(font, thetext) svg.append('<use xlink:href="#%s" transform="translate(%s,%s)scale(%s)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append(' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) ) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x,curr_y = 0.0,0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = - new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%s"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%s"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%s"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%s,%s) rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%s,%s)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append('<rect x="%s" y="%s" width="%s" height="%s" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self._svgwriter.write (''.join(svg)) self.close_group("mathtext") def finish(self): write = self._svgwriter.write if len(self._char_defs): write('<defs id="fontpaths">\n') for char_num, path in self._char_defs.values(): write(path) write('</defs>\n') write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width=width self.height=height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self.mathtext_parser = MathTextParser('SVG') svgwriter.write(svgProlog%(width,height,width,height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid if gc.get_url() is not None: self._svgwriter.write('<a xlink:href="%s">' % gc.get_url()) style = self._get_style(gc, rgbFace) self._svgwriter.write ('<%s style="%s" %s %s/>\n' % ( element, style, clippath, details)) if gc.get_url() is not None: self._svgwriter.write('</a>') def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ HATCH_SIZE = 72 dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) id = self._hatchd.get(dictkey) if id is None: id = 'h%s' % md5(str(dictkey)).hexdigest() self._svgwriter.write('<defs>\n <pattern id="%s" ' % id) self._svgwriter.write('patternUnits="userSpaceOnUse" x="0" y="0" ') self._svgwriter.write(' width="%d" height="%d" >\n' % (HATCH_SIZE, HATCH_SIZE)) path_data = self._convert_path( gc.get_hatch_path(), Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE)) if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) self._svgwriter.write( '<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (HATCH_SIZE+1, HATCH_SIZE+1, fill)) path = '<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % ( path_data, rgb2hex(gc.get_rgb()[:3]), rgb2hex(gc.get_rgb()[:3])) self._svgwriter.write(path) self._svgwriter.write('\n </pattern>\n</defs>') self._hatchd[dictkey] = id return id def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if gc.get_hatch() is not None: fill = "url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace[:3]) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % ( ','.join(['%f'%val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %f; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % ( fill, rgb2hex(gc.get_rgb()[:3]), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %f' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: path_data = self._convert_path(clippath, clippath_trans) path = '<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) path = '<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals() else: return None id = self._clipd.get(path) if id is None: id = 'p%s' % md5(path).hexdigest() self._svgwriter.write('<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write('\n </clipPath>\n</defs>') self._clipd[path] = id return id def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self._svgwriter.write('<g id="%s">\n' % (gid)) else: self._groupd[s] = self._groupd.get(s,0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] _path_commands = { Path.MOVETO: 'M%f %f', Path.LINETO: 'L%f %f', Path.CURVE3: 'Q%f %f %f %f', Path.CURVE4: 'C%f %f %f %f %f %f' } def _make_flip_transform(self, transform): return (transform + Affine2D() .scale(1.0, -1.0) .translate(0.0, self.height)) def _convert_path(self, path, transform, clip=False): path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None for points, code in path.iter_segments(transform, clip=clip): if code == Path.CLOSEPOLY: segment = 'z' else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender("\n") currpos = 0 appender(segment) currpos += len(segment) return ''.join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) path_data = self._convert_path(path, trans_and_flip, clip=(rgbFace is None)) self._draw_svg_element('path', 'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) name = self._markers.get(key) if name is None: name = 'm%s' % md5(key).hexdigest() write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>' % clippath) trans_and_flip = self._make_flip_transform(trans) for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write ('<use style="%s" %s/>\n' % (style, details)) write('</g>') def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._svgwriter.write path_codes = [] write('<defs>\n') for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform) name = 'coll%x_%x_%s' % (self._path_collection_id, i, md5(d).hexdigest()) write('<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write('</defs>\n') for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): clipid = self._get_gc_clip_svg(gc) url = gc.get_url() if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) if clipid is not None: write('<g clip-path="url(#%s)">' % clipid) details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc, rgbFace) self._svgwriter.write ('<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write('</g>') if url is not None: self._svgwriter.write('</a>') self._path_collection_id += 1 def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): # MGDTODO: Support clippath here trans = [1,0,0,1,0,0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] transstr = 'transform="matrix(%f %f %f %f %f %f)" '%tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows,numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h,w = im.get_size_out() url = getattr(im, '_url', None) if url is not None: self._svgwriter.write('<a xlink:href="%s">' % url) self._svgwriter.write ( '<image x="%f" y="%f" width="%f" height="%f" ' '%s xlink:href="'%(x/trans[0], (self.height-y)/trans[3]-h, w, h, transstr) ) if rcParams['svg.image_inline']: self._svgwriter.write("data:image/png;base64,\n") stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() self._svgwriter.write(base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename,0) + 1 filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename]) verbose.report( 'Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write('"/>\n') if url is not None: self._svgwriter.write('</a>') def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' x="%f"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ('font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f' % (fontsize, fontfamily,fontstyle, color, gc.get_alpha())) if angle!=0: transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append("c%f %f %f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = ''.join(path_data) char_num = 'c_%s' % md5(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write style = "fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append('<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append(' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) ) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x,curr_y = 0.0,0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = - new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%f,%f) rotate(%1.1f)' % (x,y,-angle) ) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append('<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self.open_group("mathtext") write (''.join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class TextToPath: """A class that converts strings to paths.""" FONT_SCALE = 100. DPI = 72 def __init__(self): self.mathtext_parser = MathTextParser('path') self._texmanager = None def _get_font(self, prop): """ Find the `FT2Font` matching font properties *prop*, with its size set. """ fname = font_manager.findfont(prop) font = get_font(fname) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode)) def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode)) return char_id @cbook.deprecated( "3.1", alternative="font.get_path() and manual translation of the vertices") def glyph_to_path(self, font, currx=0.): """Convert the *font*'s current glyph to a (vertices, codes) pair.""" verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = fontsize / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). Parameters ---------- prop : `~matplotlib.font_manager.FontProperties` The font properties for the text. s : str The text to be converted. ismath : {False, True, "TeX"} If True, use mathtext parser. If "TeX", use tex for renderering. Returns ------- verts, codes : tuple of lists *verts* is a list of numpy arrays containing the x and y coordinates of the vertices. *codes* is a list of path codes. Examples -------- Create a list of vertices and codes from a text, and create a `.Path` from those:: from matplotlib.path import Path from matplotlib.textpath import TextToPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Humor Sans", style="italic") verts, codes = TextToPath().get_text_path(fp, "ABC") path = Path(verts, codes, closed=False) Also see `TextPath` for a more direct way to create a path from a text. """ if ismath == "TeX": glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ Convert string *s* to vertices and codes using the provided ttf font. """ if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map xpositions = [] glyph_ids = [] for char, (_, x) in zip(s, _text_layout.layout(s, font)): char_id = self._get_char_id(font, ord(char)) glyph_ids.append(char_id) xpositions.append(x) if char_id not in glyph_map: glyph_map_new[char_id] = font.get_path() ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ Parse mathtext string *s* and convert it to a (vertices, codes) pair. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """Convert the string *s* to vertices and codes using usetex mode.""" # Mostly borrowed from pdf backend. dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) with dviread.Dvi(dvifile, self.DPI) as dvi: page, = dvi if glyph_map is None: glyph_map = OrderedDict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. for x1, y1, dvifont, glyph, width in page.text: font, enc = self._get_ps_font_and_encoding(dvifont.texname) char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) # See comments in _get_ps_font_and_encoding. if enc is not None: index = font.get_name_index(enc[glyph]) font.load_glyph(index, flags=LOAD_TARGET_LIGHT) else: font.load_char(glyph, flags=LOAD_TARGET_LIGHT) glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [ Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY ] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) psfont = tex_font_map[texname] if psfont.filename is None: raise ValueError( f"No usable font file found for {psfont.psname} ({texname}). " f"The font may lack a Type-1 version.") font = get_font(psfont.filename) if psfont.encoding: # If psfonts.map specifies an encoding, use it: it gives us a # mapping of glyph indices to Adobe glyph names; use it to convert # dvi indices to glyph names and use the FreeType-synthesized # unicode charmap to convert glyph names to glyph indices (with # FT_Get_Name_Index/get_name_index), and load the glyph using # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least # as good as, and possibly better than, the native charmaps.) enc = dviread._parse_enc(psfont.encoding) else: # If psfonts.map specifies no encoding, the indices directly # map to the font's "native" charmap; so don't use the # FreeType-synthesized charmap but the native ones (we can't # directly identify it but it's typically an Adobe charmap), and # directly load the dvi glyph indices using FT_Load_Char/load_char. for charmap_code in [ 1094992451, # ADOBE_CUSTOM. 1094995778, # ADOBE_STANDARD. ]: try: font.select_charmap(charmap_code) except (ValueError, RuntimeError): pass else: break else: _log.warning("No supported encoding in font (%s).", psfont.filename) enc = None return font, enc
class RendererKivy(RendererBase): '''The kivy renderer handles drawing/rendering operations. A RendererKivy should be initialized with a FigureCanvasKivy widget. On initialization a MathTextParser is instantiated to generate math text inside a FigureCanvasKivy widget. Additionally a list to store clip_rectangles is defined for elements that need to be clipped inside a rectangle such as axes. The rest of the render is performed using kivy graphics instructions. ''' def __init__(self, widget): super(RendererKivy, self).__init__() self.widget = widget self.dpi = widget.figure.dpi self._markers = {} # Can be enhanced by using TextToPath matplotlib, textpath.py self.mathtext_parser = MathTextParser("Bitmap") self.list_goraud_triangles = [] self.clip_rectangles = [] self.labels_inside_plot = [] def contains(self, widget, x, y): '''Returns whether or not a point is inside the widget. The value of the point is defined in x, y as kivy coordinates. ''' left = widget.x bottom = widget.y top = widget.y + widget.height right = widget.x + widget.width return (left <= x <= right and bottom <= y <= top) def handle_clip_rectangle(self, gc, x, y): '''It checks whether the point (x,y) collides with any already existent stencil. If so it returns the index position of the stencil it collides with. if the new clip rectangle bounds are None it draws in the canvas otherwise it finds the correspondent stencil or creates a new one for the new graphics instructions. The point x,y is given in matplotlib coordinates. ''' x = self.widget.x + x y = self.widget.y + y collides = self.collides_with_existent_stencil(x, y) if collides > -1: return collides new_bounds = gc.get_clip_rectangle() if new_bounds: x = self.widget.x + int(new_bounds.bounds[0]) y = self.widget.y + int(new_bounds.bounds[1]) w = int(new_bounds.bounds[2]) h = int(new_bounds.bounds[3]) collides = self.collides_with_existent_stencil(x, y) if collides == -1: cliparea = StencilView(pos=(x, y), size=(w, h)) self.clip_rectangles.append(cliparea) self.widget.add_widget(cliparea) return len(self.clip_rectangles) - 1 else: return collides else: return -2 def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): '''Draws a collection of paths selecting drawing properties from the lists *facecolors*, *edgecolors*, *linewidths*, *linestyles* and *antialiaseds*. *offsets* is a list of offsets to apply to each of the paths. The offsets in *offsets* are first transformed by *offsetTrans* before being applied. *offset_position* may be either "screen" or "data" depending on the space that the offsets are in. ''' len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) # check whether an optimization is needed by calculating the cost of # generating and use a path with the cost of emitting a path in-line. should_do_optimization = \ len_path + uses_per_path + 5 < len_path * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) # Generate an array of unique paths with the respective transformations path_codes = [] for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) polygons = path.to_polygons(transform) path_codes.append(polygons) # Apply the styles and rgbFace to each one of the raw paths from # the list. Additionally a transformation is being applied to # translate each independent path for xo, yo, path_poly, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): list_canvas_instruction = self.get_path_instructions( gc0, path_poly, closed=True, rgbFace=rgbFace) for widget, instructions in list_canvas_instruction: widget.canvas.add(PushMatrix()) widget.canvas.add(Translate(xo, yo)) widget.canvas.add(instructions) widget.canvas.add(PopMatrix()) def collides_with_existent_stencil(self, x, y): '''Check all the clipareas and returns the index of the clip area that contains this point. The point x, y is given in kivy coordinates. ''' idx = -1 for cliparea in self.clip_rectangles: idx += 1 if self.contains(cliparea, x, y): return idx return -1 def get_path_instructions(self, gc, polygons, closed=False, rgbFace=None): '''With a graphics context and a set of polygons it returns a list of InstructionGroups required to render the path. ''' instructions_list = [] points_line = [] for polygon in polygons: for x, y in polygon: x = x + self.widget.x y = y + self.widget.y points_line += [ float(x), float(y), ] tess = Tesselator() tess.add_contour(points_line) if not tess.tesselate(): Logger.warning("Tesselator didn't work :(") return newclip = self.handle_clip_rectangle(gc, x, y) if newclip > -1: instructions_list.append((self.clip_rectangles[newclip], self.get_graphics(gc, tess, points_line, rgbFace, closed=closed))) else: instructions_list.append((self.widget, self.get_graphics(gc, tess, points_line, rgbFace, closed=closed))) return instructions_list def get_graphics(self, gc, polygons, points_line, rgbFace, closed=False): '''Return an instruction group which contains the necessary graphics instructions to draw the respective graphics. ''' instruction_group = InstructionGroup() if isinstance(gc.line['dash_list'], tuple): gc.line['dash_list'] = list(gc.line['dash_list']) if rgbFace is not None: if len(polygons.meshes) != 0: instruction_group.add(Color(*rgbFace)) for vertices, indices in polygons.meshes: instruction_group.add( Mesh(vertices=vertices, indices=indices, mode=str("triangle_fan"))) instruction_group.add(Color(*gc.get_rgb())) if _mpl_1_5 and closed: points_poly_line = points_line[:-2] else: points_poly_line = points_line if gc.line['width'] > 0: instruction_group.add( Line(points=points_poly_line, width=int(gc.line['width'] / 2), dash_length=gc.line['dash_length'], dash_offset=gc.line['dash_offset'], dash_joint=gc.line['joint_style'], dash_list=gc.line['dash_list'])) return instruction_group def draw_image(self, gc, x, y, im): '''Render images that can be displayed on a matplotlib figure. These images are generally called using imshow method from pyplot. A Texture is applied to the FigureCanvas. The position x, y is given in matplotlib coordinates. ''' # Clip path to define an area to mask. clippath, clippath_trans = gc.get_clip_path() # Normal coordinates calculated and image added. x = self.widget.x + x y = self.widget.y + y bbox = gc.get_clip_rectangle() if bbox is not None: l, b, w, h = bbox.bounds else: l = 0 b = 0 w = self.widget.width h = self.widget.height h, w = im.get_size_out() rows, cols, image_str = im.as_rgba_str() texture = Texture.create(size=(w, h)) texture.blit_buffer(image_str, colorfmt='rgba', bufferfmt='ubyte') if clippath is None: with self.widget.canvas: Color(1.0, 1.0, 1.0, 1.0) Rectangle(texture=texture, pos=(x, y), size=(w, h)) else: polygons = clippath.to_polygons(clippath_trans) list_canvas_instruction = self.get_path_instructions( gc, polygons, rgbFace=(1.0, 1.0, 1.0, 1.0)) for widget, instructions in list_canvas_instruction: widget.canvas.add(StencilPush()) widget.canvas.add(instructions) widget.canvas.add(StencilUse()) widget.canvas.add(Color(1.0, 1.0, 1.0, 1.0)) widget.canvas.add( Rectangle(texture=texture, pos=(x, y), size=(w, h))) widget.canvas.add(StencilUnUse()) widget.canvas.add(StencilPop()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): '''Render text that is displayed in the canvas. The position x, y is given in matplotlib coordinates. A `GraphicsContextKivy` is given to render according to the text properties such as color, size, etc. An angle is given to change the orientation of the text when needed. If the text is a math expression it will be rendered using a MathText parser. ''' if mtext: transform = mtext.get_transform() ax, ay = transform.transform_point(mtext.get_position()) angle_rad = mtext.get_rotation() * np.pi / 180. dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) if mtext.get_rotation_mode() == "anchor": # if anchor mode, rotation is undone first v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] w, h, d = self.get_text_width_height_descent(s, prop, ismath) ha, va = mtext.get_ha(), mtext.get_va() if ha == "center": ax -= w / 2 elif ha == "right": ax -= w if va == "top": ay -= h elif va == "center": ay -= h / 2 if mtext.get_rotation_mode() != "anchor": # if not anchor mode, rotation is undone last v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] x, y = ax, ay x += self.widget.x y += self.widget.y if ismath: self.draw_mathtext(gc, x, y, s, prop, angle) else: font = resource_find(prop.get_name() + ".ttf") if font is None: plot_text = CoreLabel(font_size=prop.get_size_in_points()) else: plot_text = CoreLabel(font_size=prop.get_size_in_points(), font_name=prop.get_name()) plot_text.text = six.text_type("{}".format(s)) if prop.get_style() == 'italic': plot_text.italic = True if weight_as_number(prop.get_weight()) > 500: plot_text.bold = True plot_text.refresh() with self.widget.canvas: if isinstance(angle, float): PushMatrix() Rotate(angle=angle, origin=(int(x), int(y))) Rectangle(pos=(int(x), int(y)), texture=plot_text.texture, size=plot_text.texture.size) PopMatrix() else: Rectangle(pos=(int(x), int(y)), texture=plot_text.texture, size=plot_text.texture.size) def draw_mathtext(self, gc, x, y, s, prop, angle): '''Draw the math text using matplotlib.mathtext. The position x,y is given in Kivy coordinates. ''' ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop) w = ftimage.get_width() h = ftimage.get_height() texture = Texture.create(size=(w, h)) if _mpl_1_5: texture.blit_buffer(ftimage.as_rgba_str()[0][0], colorfmt='rgba', bufferfmt='ubyte') else: texture.blit_buffer(ftimage.as_rgba_str(), colorfmt='rgba', bufferfmt='ubyte') texture.flip_vertical() with self.widget.canvas: Rectangle(texture=texture, pos=(x, y), size=(w, h)) def draw_path(self, gc, path, transform, rgbFace=None): '''Produce the rendering of the graphics elements using :class:`kivy.graphics.Line` and :class:`kivy.graphics.Mesh` kivy graphics instructions. The paths are converted into polygons and assigned either to a clip rectangle or to the same canvas for rendering. Paths are received in matplotlib coordinates. The aesthetics is defined by the `GraphicsContextKivy` gc. ''' polygons = path.to_polygons(transform, self.widget.width, self.widget.height) list_canvas_instruction = self.get_path_instructions(gc, polygons, closed=True, rgbFace=rgbFace) for widget, instructions in list_canvas_instruction: widget.canvas.add(instructions) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): '''Markers graphics instructions are stored on a dictionary and hashed through graphics context and rgbFace values. If a marker_path with the corresponding graphics context exist then the instructions are pulled from the markers dictionary. ''' if not len(path.vertices): return # get a string representation of the path path_data = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) # get a string representation of the graphics context and rgbFace. style = str(gc._get_style_dict(rgbFace)) dictkey = (path_data, str(style)) # check whether this marker has been created before. list_instructions = self._markers.get(dictkey) # creating a list of instructions for the specific marker. if list_instructions is None: polygons = marker_path.to_polygons(marker_trans) self._markers[dictkey] = self.get_path_instructions( gc, polygons, rgbFace=rgbFace) # Traversing all the positions where a marker should be rendered for vertices, codes in path.iter_segments(trans, simplify=False): if len(vertices): x, y = vertices[-2:] for widget, instructions in self._markers[dictkey]: widget.canvas.add(PushMatrix()) widget.canvas.add(Translate(x, y)) widget.canvas.add(instructions) widget.canvas.add(PopMatrix()) def flipy(self): return False def _convert_path(self, path, transform=None, clip=None, simplify=None, sketch=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None if _mpl_1_5: return _path.convert_to_string(path, transform, clip, simplify, sketch, 6, [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') else: return _path.convert_to_svg(path, transform, clip, simplify, 6) def get_canvas_width_height(self): '''Get the actual width and height of the widget. ''' return self.widget.width, self.widget.height def get_text_width_height_descent(self, s, prop, ismath): '''This method is needed specifically to calculate text positioning in the canvas. Matplotlib needs the size to calculate the points according to their layout ''' if ismath: ftimage, depth = self.mathtext_parser.parse(s, self.dpi, prop) w = ftimage.get_width() h = ftimage.get_height() return w, h, depth font = resource_find(prop.get_name() + ".ttf") if font is None: plot_text = CoreLabel(font_size=prop.get_size_in_points()) else: plot_text = CoreLabel(font_size=prop.get_size_in_points(), font_name=prop.get_name()) plot_text.text = six.text_type("{}".format(s)) plot_text.refresh() return plot_text.texture.size[0], plot_text.texture.size[1], 1 def new_gc(self): '''Instantiate a GraphicsContextKivy object ''' return GraphicsContextKivy(self.widget) def points_to_pixels(self, points): return points / 72.0 * self.dpi
class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, 200 : cairo.FONT_WEIGHT_NORMAL, 300 : cairo.FONT_WEIGHT_NORMAL, 400 : cairo.FONT_WEIGHT_NORMAL, 500 : cairo.FONT_WEIGHT_NORMAL, 600 : cairo.FONT_WEIGHT_BOLD, 700 : cairo.FONT_WEIGHT_BOLD, 800 : cairo.FONT_WEIGHT_BOLD, 900 : cairo.FONT_WEIGHT_BOLD, 'ultralight' : cairo.FONT_WEIGHT_NORMAL, 'light' : cairo.FONT_WEIGHT_NORMAL, 'normal' : cairo.FONT_WEIGHT_NORMAL, 'medium' : cairo.FONT_WEIGHT_NORMAL, 'semibold' : cairo.FONT_WEIGHT_BOLD, 'bold' : cairo.FONT_WEIGHT_BOLD, 'heavy' : cairo.FONT_WEIGHT_BOLD, 'ultrabold' : cairo.FONT_WEIGHT_BOLD, 'black' : cairo.FONT_WEIGHT_BOLD, } fontangles = { 'italic' : cairo.FONT_SLANT_ITALIC, 'normal' : cairo.FONT_SLANT_NORMAL, 'oblique' : cairo.FONT_SLANT_OBLIQUE, } def __init__(self, dpi): """ """ if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.dpi = dpi self.gc = GraphicsContextCairo (renderer=self) self.text_ctx = cairo.Context ( cairo.ImageSurface (cairo.FORMAT_ARGB32,1,1)) self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) def set_ctx_from_surface (self, surface): self.gc.ctx = cairo.Context (surface) def set_width_height(self, width, height): self.width = width self.height = height self.matrix_flipy = cairo.Matrix (yy=-1, y0=self.height) # use matrix_flipy for ALL rendering? # - problem with text? - will need to switch matrix_flipy off, or do a # font transform? def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides): if fill_c is not None: ctx.save() if len(fill_c) == 3 or alpha_overrides: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], alpha) else: ctx.set_source_rgba (fill_c[0], fill_c[1], fill_c[2], fill_c[3]) ctx.fill_preserve() ctx.restore() ctx.stroke() @staticmethod def convert_path(ctx, path, transform, clip=None): for points, code in path.iter_segments(transform, clip=clip): if code == Path.MOVETO: ctx.move_to(*points) elif code == Path.CLOSEPOLY: ctx.close_path() elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: ctx.curve_to(points[0], points[1], points[0], points[1], points[2], points[3]) elif code == Path.CURVE4: ctx.curve_to(*points) def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx # We'll clip the path to the actual rendering extents # if the path isn't filled. if rgbFace is None and gc.get_hatch() is None: clip = ctx.clip_extents() else: clip = None transform = transform + \ Affine2D().scale(1.0, -1.0).translate(0, self.height) ctx.new_path() self.convert_path(ctx, path, transform, clip) self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_image(self, gc, x, y, im): # bbox - not currently used if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if sys.byteorder == 'little': im = im[:, :, (2, 1, 0, 3)] else: im = im[:, :, (3, 0, 1, 2)] surface = cairo.ImageSurface.create_for_data( memoryview(im.flatten()), cairo.FORMAT_ARGB32, im.shape[1], im.shape[0], im.shape[1]*4) ctx = gc.ctx y = self.height - y - im.shape[0] ctx.save() ctx.set_source_surface(surface, float(x), float(y)) if gc.get_alpha() != 1.0: ctx.paint_with_alpha(gc.get_alpha()) else: ctx.paint() ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # Note: x,y are device/display coords, not user-coords, unlike other # draw_* methods if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: ctx = gc.ctx ctx.new_path() ctx.move_to (x, y) ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) size = prop.get_size_in_points() * self.dpi / 72.0 ctx.save() if angle: ctx.rotate (-angle * np.pi / 180) ctx.set_font_size (size) if HAS_CAIRO_CFFI: if not isinstance(s, six.text_type): s = six.text_type(s) else: if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) ctx.restore() def _draw_mathtext(self, gc, x, y, s, prop, angle): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) ctx = gc.ctx width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate (-angle * np.pi / 180) for font, fontsize, s, ox, oy in glyphs: ctx.new_path() ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) size = fontsize * self.dpi / 72.0 ctx.set_font_size(size) if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) ctx.restore() for ox, oy, w, h in rects: ctx.new_path() ctx.rectangle (ox, oy, w, h) ctx.set_source_rgb (0, 0, 0) ctx.fill_preserve() ctx.restore() def flipy(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return True #return False # tried - all draw objects ok except text (and images?) # which comes out mirrored! def get_canvas_width_height(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) if ismath: width, height, descent, fonts, used_characters = self.mathtext_parser.parse( s, self.dpi, prop) return width, height, descent ctx = self.text_ctx ctx.save() ctx.select_font_face (prop.get_name(), self.fontangles [prop.get_style()], self.fontweights[prop.get_weight()]) # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c # but if /96.0 is used the font is too small size = prop.get_size_in_points() * self.dpi / 72.0 # problem - scale remembers last setting and font can become # enormous causing program to crash # save/restore prevents the problem ctx.set_font_size (size) y_bearing, w, h = ctx.text_extents (s)[1:4] ctx.restore() return w, h, h + y_bearing def new_gc(self): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) self.gc.ctx.save() self.gc._alpha = 1.0 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def points_to_pixels(self, points): if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name())) return points/72.0 * self.dpi
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()
class TextToPath(object): """ A class that convert a given text to a path using ttf fonts. """ FONT_SCALE = 50. DPI = 72 def __init__(self): """ Initialization """ self.mathtext_parser = MathTextParser('path') self.tex_font_map = None from matplotlib.cbook import maxdict self._ps_fontd = maxdict(50) self._texmanager = None def _get_font(self, prop): """ find a ttf font. """ fname = font_manager.findfont(prop) font = FT2Font(str(fname)) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ ps_name = font.get_sfnt()[(1,0,0,6)] char_id = urllib.quote('%s-%d' % (ps_name, ccode)) return char_id def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib.quote('%s-%d' % (ps_name, ccode)) return char_id def glyph_to_path(self, glyph, currx=0.): """ convert the ft2font glyph to vertices and codes. """ #Mostly copied from backend_svg.py. verts, codes = [], [] for step in glyph.path: if step[0] == 0: # MOVE_TO verts.append((step[1], step[2])) codes.append(Path.MOVETO) elif step[0] == 1: # LINE_TO verts.append((step[1], step[2])) codes.append(Path.LINETO) elif step[0] == 2: # CURVE3 verts.extend([(step[1], step[2]), (step[3], step[4])]) codes.extend([Path.CURVE3, Path.CURVE3]) elif step[0] == 3: # CURVE4 verts.extend([(step[1], step[2]), (step[3], step[4]), (step[5], step[6])]) codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4]) elif step[0] == 4: # ENDPOLY verts.append((0, 0,)) codes.append(Path.CLOSEPOLY) verts = [(x+currx, y) for (x,y) in verts] return verts, codes def get_text_path(self, prop, s, ismath=False, usetex=False): """ convert text *s* to path (a tuple of vertices and codes for matplotlib.math.Path). *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. Effective only if usetex == False. """ if usetex==False: if ismath == False: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if verts1: verts1 = np.array(verts1)*scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. cmap = font.get_charmap() lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = (glyph.linearHoriAdvance / 65536.0) char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: glyph_map_new[char_id] = self.glyph_to_path(glyph) currx += (kern / 64.0) xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map_new, rects def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes by parsing it with mathtext. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] currx, curry = 0, 0 for font, fontsize, s, ox, oy in glyphs: ccode = ord(s) char_id = self._get_char_id(font, ccode) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = self.glyph_to_path(glyph) xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1=[(ox, oy), (ox, oy+h), (ox+w, oy+h), (ox+w, oy), (ox, oy), (0,0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return zip(glyph_ids, xpositions, ypositions, sizes), glyph_map, myrects def get_texmanager(self): """ return the :class:`matplotlib.texmanager.TexManager` instance """ if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using matplotlib's usetex mode. """ # codes are modstly borrowed from pdf backend. texmanager = self.get_texmanager() if self.tex_font_map is None: self.tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) fontsize = prop.get_size_in_points() if hasattr(texmanager, "get_dvi"): # dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE) dvi = dviread.DviFromFileLike(dvifilelike, self.DPI) else: dvifile = texmanager.make_dvi(s, self.FONT_SCALE) dvi = dviread.Dvi(dvifile, self.DPI) page = iter(dvi).next() dvi.close() if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. #oldfont, seq = None, [] for x1, y1, dvifont, glyph, width in page.text: font_and_encoding = self._ps_fontd.get(dvifont.texname) if font_and_encoding is None: font_bunch = self.tex_font_map[dvifont.texname] font = FT2Font(font_bunch.filename) try: font.select_charmap(1094992451) # select ADOBE_CUSTOM except ValueError: font.set_charmap(0) if font_bunch.encoding: enc = dviread.Encoding(font_bunch.encoding) else: enc = None self._ps_fontd[dvifont.texname] = font, enc else: font, enc = font_and_encoding ft2font_flag = LOAD_TARGET_LIGHT char_id = self._get_char_id_ps(font, glyph) if not char_id in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = self.glyph_to_path(glyph0) glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size/self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1=[(ox, oy), (ox+w, oy), (ox+w, oy+h), (ox, oy+h), (ox, oy), (0,0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return zip(glyph_ids, xpositions, ypositions, sizes), \ glyph_map_new, myrects
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._has_gouraud = False self._n_gradients = 0 self._fonts = {} self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', width='%ipt' % width, height='%ipt' % height, viewBox='0 0 %i %i' % (width, height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) def _write_default_style(self): writer = self.writer default_style = generate_css({ 'stroke-linejoin': 'round', 'stroke-linecap': 'square' }) writer.start('defs') writer.start('style', type='text/css') writer.data('*{%s}\n' % default_style) writer.end('style') writer.end('defs') def _make_id(self, type, content): return '%s%s' % (type, md5(str(content)).hexdigest()[:10]) def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, gc.get_rgb()), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start('defs') for ((path, face, stroke), oid) in self._hatchd.values(): writer.start('pattern', id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=str(HATCH_SIZE), height=str(HATCH_SIZE)) path_data = self._convert_path(path, Affine2D().scale(HATCH_SIZE).scale( 1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if face is None: fill = 'none' else: fill = rgb2hex(face) writer.element('rect', x="0", y="0", width=str(HATCH_SIZE + 1), height=str(HATCH_SIZE + 1), fill=fill) writer.element('path', d=path_data, style=generate_css({ 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), 'stroke-width': str(1.0), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' })) writer.end('pattern') writer.end('defs') def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace """ attrib = {} if gc.get_hatch() is not None: attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: attrib['fill'] = 'none' elif tuple(rgbFace[:3]) != (0, 0, 0): attrib['fill'] = rgb2hex(rgbFace) if gc.get_alpha() != 1.0: attrib['opacity'] = str(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib['stroke-dasharray'] = ','.join(['%f' % val for val in seq]) attrib['stroke-dashoffset'] = str(float(offset)) linewidth = gc.get_linewidth() if linewidth: attrib['stroke'] = rgb2hex(gc.get_rgb()) if linewidth != 1.0: attrib['stroke-width'] = str(linewidth) if gc.get_joinstyle() != 'round': attrib['stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'projecting': attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id('p', dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start('defs') for clip, oid in self._clipd.values(): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element('path', d=path_data) else: x, y, w, h = clip writer.element('rect', x=str(x), y=str(y), width=str(w), height=str(h)) writer.end('clipPath') writer.end('defs') def _write_svgfonts(self): if not rcParams['svg.fonttype'] == 'svgfont': return writer = self.writer writer.start('defs') for font_fname, chars in self._fonts.items(): font = FT2Font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() writer.start('font', id=sfnt[(1, 0, 0, 4)]) writer.element('font-face', attrib={ 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'units-per-em': '72', 'bbox': ' '.join(str(x / 64.0) for x in font.bbox) }) for char in chars: glyph = font.load_char(char, flags=LOAD_NO_HINTING) verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) # name = font.get_glyph_name(char) writer.element( 'glyph', d=path_data, attrib={ # 'glyph-name': name, 'unicode': unichr(char), 'horiz-adv-x': str(glyph.linearHoriAdvance / 65536.0) }) writer.end('font') writer.end('defs') def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start('g', id="%s_%d" % (s, self._groupd[s])) def close_group(self, s): self.writer.end('g') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] def _convert_path(self, path, transform=None, clip=None, simplify=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_svg(path, transform, clip, simplify, 6) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify) attrib = {} attrib['style'] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) self.writer.element('path', d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end('</a>') def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if not len(path.vertices): return writer = self.writer path_data = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) for key in style.keys(): if not key.startswith('stroke'): del style[key] style = generate_css(style) if oid is None: oid = self._make_id('m', dictkey) writer.start('defs') writer.element('path', id=oid, d=path_data, style=style) writer.end('defs') self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid writer.start('g', attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': '#%s' % oid} for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib['x'] = str(x) attrib['y'] = str(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): writer = self.writer path_codes = [] writer.start('defs') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = 'C%x_%x_%s' % (self._path_collection_id, i, self._make_id('', d)) writer.element('path', id=oid, d=d) path_codes.append(oid) writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) if clipid is not None: writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) attrib = { 'xlink:href': '#%s' % path_id, 'x': str(xo), 'y': str(self.height - yo), 'style': self._get_style(gc0, rgbFace) } writer.element('use', attrib=attrib) if clipid is not None: writer.end('g') if url is not None: writer.end('a') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start('filter', id='colorAdd') writer.element('feComposite', attrib={'in': 'SourceGraphic'}, in2='BackgroundImage', operator='arithmetic', k2="1", k3="1") writer.end('filter') avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start('defs') for i in range(3): x1, y1 = points[i] x2, y2 = points[(i + 1) % 3] x3, y3 = points[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start('linearGradient', id="GR%x_%d" % (self._n_gradients, i), x1=str(x1), y1=str(y1), x2=str(xb), y2=str(yb)) writer.element('stop', offset='0', style=generate_css({ 'stop-color': rgb2hex(c), 'stop-opacity': str(c[-1]) })) writer.element('stop', offset='1', style=generate_css({ 'stop-color': rgb2hex(c), 'stop-opacity': "0" })) writer.end('linearGradient') writer.element('polygon', id='GT%x' % self._n_gradients, points=" ".join( [str(x) for x in x1, y1, x2, y2, x3, y3])) writer.end('defs') avg_color = np.sum(colors[:, :], axis=0) / 3.0 href = '#GT%x' % self._n_gradients writer.element('use', attrib={ 'xlink:href': href, 'fill': rgb2hex(avg_color), 'fill-opacity': str(avg_color[-1]) }) for i in range(3): writer.element('use', attrib={ 'xlink:href': href, 'fill': 'url(#GR%x_%d)' % (self._n_gradients, i), 'fill-opacity': '1', 'filter': 'url(#colorAdd)' }) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid self.writer.start('g', attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end('g') def option_scale_image(self): return True def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) trans = [1, 0, 0, 1, 0, 0] if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] attrib['transform'] = generate_transform([('matrix', tuple(trans)) ]) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() url = getattr(im, '_url', None) if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if rcParams['svg.image_inline']: stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() attrib['xlink:href'] = ("data:image/png;base64,\n" + base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() attrib['xlink:href'] = filename alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = str(alpha) if transform is None: self.writer.element('image', x=str(x / trans[0]), y=str((self.height - y) / trans[3] - h), width=str(w), height=str(h), attrib=attrib) else: flipped = self._make_flip_transform(transform) attrib['transform'] = generate_transform([('matrix', flipped.to_values())]) self.writer.element('image', x=str(x), y=str(y + dy), width=str(dx), height=str(-dy), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = str(gc.get_alpha()) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs y -= ((font.get_descent() / 64.0) * (prop.get_size_in_points() / text2path.FONT_SCALE)) if glyph_map_new: writer.start('defs') for char_id, glyph_path in glyph_map_new.iteritems(): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} attrib['style'] = generate_css(style) font_scale = fontsize / text2path.FONT_SCALE attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle, )), ('scale', (font_scale, -font_scale)) ]) writer.start('g', attrib=attrib) for glyph_id, xposition, yposition, scale in glyph_info: attrib = {'xlink:href': '#%s' % glyph_id} if xposition != 0.0: attrib['x'] = str(xposition) if yposition != 0.0: attrib['y'] = str(yposition) writer.element('use', attrib=attrib) writer.end('g') else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext( prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: writer.start('defs') for char_id, glyph_path in glyph_map_new.iteritems(): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): path_data = "" else: path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') glyph_map.update(glyph_map_new) attrib = {} font_scale = fontsize / text2path.FONT_SCALE attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle, )), ('scale', (font_scale, -font_scale)) ]) writer.start('g', attrib=attrib) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element('use', transform=generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale, )), ]), attrib={'xlink:href': '#%s' % char_id}) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element('path', d=path_data) writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style['opacity'] = str(gc.get_alpha()) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() fontfamily = font.family_name fontstyle = prop.get_style() attrib = {} # Must add "px" to workaround a Firefox bug style['font-size'] = str(fontsize) + 'px' style['font-family'] = str(fontfamily) style['font-style'] = prop.get_style().lower() attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([('translate', (x, y)), ('rotate', (-angle, ))]) writer.element('text', s, attrib=attrib) if rcParams['svg.fonttype'] == 'svgfont': fontset = self._fonts.setdefault(font.fname, set()) for c in s: fontset.add(ord(c)) else: writer.comment(s) width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([('translate', (x, y)), ('rotate', (-angle, ))]) # Apply attributes to 'g', not 'text', because we likely # have some rectangles as well with the same style and # transformation writer.start('g', attrib=attrib) writer.start('text') # Sort the characters by font, and output one tspan for # each spans = {} for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css({ # Must add "px" to work around a Firefox bug 'font-size': str(fontsize) + 'px', 'font-family': font.family_name, 'font-style': font.style_name.lower() }) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) if rcParams['svg.fonttype'] == 'svgfont': for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) for style, chars in spans.items(): chars.sort() same_y = True if len(chars) > 1: last_y = chars[0][1] for i in xrange(1, len(chars)): if chars[i][1] != last_y: same_y = False break if same_y: ys = str(chars[0][1]) else: ys = ' '.join(str(c[1]) for c in chars) attrib = { 'style': style, 'x': ' '.join(str(c[0]) for c in chars), 'y': ys } writer.element('tspan', ''.join(unichr(c[2]) for c in chars), attrib=attrib) writer.end('text') if len(svg_rects): for x, y, width, height in svg_rects: writer.element('rect', x=str(x), y=str(-y + height), width=str(width), height=str(height)) writer.end('g') def draw_tex(self, gc, x, y, s, prop, angle): self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath): clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) if rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath) if clipid is not None: self.writer.end('g') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): return self._text2path.get_text_width_height_descent(s, prop, ismath)
class TextToPath(object): """ A class that convert a given text to a path using ttf fonts. """ FONT_SCALE = 100. DPI = 72 def __init__(self): """ Initialization """ self.mathtext_parser = MathTextParser('path') self.tex_font_map = None from matplotlib.cbook import maxdict self._ps_fontd = maxdict(50) self._texmanager = None self._adobe_standard_encoding = None def _get_adobe_standard_encoding(self): enc_name = dviread.find_tex_file('8a.enc') enc = dviread.Encoding(enc_name) return dict([(c, i) for i, c in enumerate(enc.encoding)]) def _get_font(self, prop): """ find a ttf font. """ fname = font_manager.findfont(prop) font = get_font(fname) font.set_size(self.FONT_SCALE, self.DPI) return font def _get_hinting_flag(self): return LOAD_NO_HINTING def _get_char_id(self, font, ccode): """ Return a unique id for the given font and character-code set. """ sfnt = font.get_sfnt() try: ps_name = sfnt[(1, 0, 0, 6)].decode('macroman') except KeyError: ps_name = sfnt[(3, 1, 0x0409, 6)].decode('utf-16be') char_id = urllib_quote('%s-%x' % (ps_name, ccode)) return char_id def _get_char_id_ps(self, font, ccode): """ Return a unique id for the given font and character-code set (for tex). """ ps_name = font.get_ps_font_info()[2] char_id = urllib_quote('%s-%d' % (ps_name, ccode)) return char_id def glyph_to_path(self, font, currx=0.): """ convert the ft2font glyph to vertices and codes. """ verts, codes = font.get_path() if currx != 0.0: verts[:, 0] += currx return verts, codes def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=None) return w, h, d fontsize = prop.get_size_in_points() scale = float(fontsize) / self.FONT_SCALE if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w * scale, h * scale, d * scale def get_text_path(self, prop, s, ismath=False, usetex=False): """ convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. Effective only if usetex == False. """ if not usetex: if not ismath: font = self._get_font(prop) glyph_info, glyph_map, rects = self.get_glyphs_with_font( font, s) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext( prop, s) else: glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) verts, codes = [], [] for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] if len(verts1): verts1 = np.array(verts1) * scale + [xposition, yposition] verts.extend(verts1) codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using the provided ttf font. """ # Mostly copied from backend_svg.py. lastgind = None currx = 0 xpositions = [] glyph_ids = [] if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = OrderedDict() else: glyph_map_new = glyph_map # I'm not sure if I get kernings right. Needs to be verified. -JJL for c in s: ccode = ord(c) gind = font.get_char_index(ccode) if gind is None: ccode = ord('?') gind = 0 if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) horiz_advance = (glyph.linearHoriAdvance / 65536.0) char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: glyph_map_new[char_id] = self.glyph_to_path(font) currx += (kern / 64.0) xpositions.append(currx) glyph_ids.append(char_id) currx += horiz_advance lastgind = gind ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, rects) def get_glyphs_mathtext(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes by parsing it with mathtext. """ prop = prop.copy() prop.set_size(self.FONT_SCALE) width, height, descent, glyphs, rects = self.mathtext_parser.parse( s, self.DPI, prop) if not glyph_map: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map xpositions = [] ypositions = [] glyph_ids = [] sizes = [] currx, curry = 0, 0 for font, fontsize, ccode, ox, oy in glyphs: char_id = self._get_char_id(font, ccode) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) glyph_map_new[char_id] = self.glyph_to_path(font) xpositions.append(ox) ypositions.append(oy) glyph_ids.append(char_id) size = fontsize / self.FONT_SCALE sizes.append(size) myrects = [] for ox, oy, w, h in rects: vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h), (ox + w, oy), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) def get_texmanager(self): """ return the :class:`matplotlib.texmanager.TexManager` instance """ if self._texmanager is None: from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager def get_glyphs_tex(self, prop, s, glyph_map=None, return_new_glyphs_only=False): """ convert the string *s* to vertices and codes using matplotlib's usetex mode. """ # codes are modstly borrowed from pdf backend. texmanager = self.get_texmanager() if self.tex_font_map is None: self.tex_font_map = dviread.PsfontsMap( dviread.find_tex_file('pdftex.map')) if self._adobe_standard_encoding is None: self._adobe_standard_encoding = self._get_adobe_standard_encoding() fontsize = prop.get_size_in_points() if hasattr(texmanager, "get_dvi"): dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE) dvi = dviread.DviFromFileLike(dvifilelike, self.DPI) else: dvifile = texmanager.make_dvi(s, self.FONT_SCALE) dvi = dviread.Dvi(dvifile, self.DPI) with dvi: page = next(iter(dvi)) if glyph_map is None: glyph_map = dict() if return_new_glyphs_only: glyph_map_new = dict() else: glyph_map_new = glyph_map glyph_ids, xpositions, ypositions, sizes = [], [], [], [] # Gather font information and do some setup for combining # characters into strings. # oldfont, seq = None, [] for x1, y1, dvifont, glyph, width in page.text: font_and_encoding = self._ps_fontd.get(dvifont.texname) font_bunch = self.tex_font_map[dvifont.texname] if font_and_encoding is None: font = get_font(font_bunch.filename) for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451), ("ADOBE_STANDARD", 1094995778)]: try: font.select_charmap(charmap_code) except (ValueError, RuntimeError): pass else: break else: charmap_name = "" warnings.warn("No supported encoding in font (%s)." % font_bunch.filename) if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding: enc0 = dviread.Encoding(font_bunch.encoding) enc = dict([(i, self._adobe_standard_encoding.get(c, None)) for i, c in enumerate(enc0.encoding)]) else: enc = dict() self._ps_fontd[dvifont.texname] = font, enc else: font, enc = font_and_encoding ft2font_flag = LOAD_TARGET_LIGHT char_id = self._get_char_id_ps(font, glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) if enc: charcode = enc.get(glyph, None) else: charcode = glyph if charcode is not None: glyph0 = font.load_char(charcode, flags=ft2font_flag) else: warnings.warn("The glyph (%d) of font (%s) cannot be " "converted with the encoding. Glyph may " "be wrong" % (glyph, font_bunch.filename)) glyph0 = font.load_char(glyph, flags=ft2font_flag) glyph_map_new[char_id] = self.glyph_to_path(font) glyph_ids.append(char_id) xpositions.append(x1) ypositions.append(y1) sizes.append(dvifont.size / self.FONT_SCALE) myrects = [] for ox, oy, h, w in page.boxes: vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h), (ox, oy + h), (ox, oy), (0, 0)] code1 = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] myrects.append((vert1, code1)) return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects)
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug=1 # we want to cache the fonts at the class level so that when # multiple figures are created we can reuse them. This helps with # a bug on windows where the creation of too many figures leads to # too many open file handles. However, storing them at the class # level is not thread safe. The solution here is to let the # FigureCanvas acquire a lock on the fontd at the start of the # draw, and release it when it is done. This allows multiple # renderers to share the cached fonts, but only one figure can # draw at at time and so the font cache is used by only one # renderer at a time lock = threading.RLock() def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def __getstate__(self): # We only want to preserve the init keywords of the Renderer. # Anything else can be re-created. return {'width': self.width, 'height': self.height, 'dpi': self.dpi} def __setstate__(self, state): self.__init__(state['width'], state['height'], state['dpi']) def _get_hinting_flag(self): if rcParams['text.hinting']: return LOAD_FORCE_AUTOHINT else: return LOAD_NO_HINTING # for filtering to work with rasterization, methods needs to be wrapped. # maybe there is better way to do it. def draw_markers(self, *kl, **kw): return self._renderer.draw_markers(*kl, **kw) def draw_path_collection(self, *kl, **kw): return self._renderer.draw_path_collection(*kl, **kw) def _update_methods(self): self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.get_content_extents = self._renderer.get_content_extents def tostring_rgba_minimized(self): extents = self.get_content_extents() bbox = [[extents[0], self.height - (extents[1] + extents[3])], [extents[0] + extents[2], self.height - extents[1]]] region = self.copy_from_bbox(bbox) return np.array(region), extents def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = np.ceil(npts/float(nmax)) chsize = int(np.ceil(npts/nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1,:] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) xd = descent * sin(radians(angle)) yd = descent * cos(radians(angle)) x = np.round(x + ox + xd) y = np.round(y - oy + yd) self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=flags) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) d = font.get_descent() / 64.0 # The descent needs to be adjusted for the angle xo, yo = font.get_bitmap_offset() xo /= 64.0 yo /= 64.0 xd = -d * sin(radians(angle)) yd = d * cos(radians(angle)) #print x, y, int(x), int(y), s self._renderer.draw_text_image( font, round(x - xd + xo), round(y + yd + yo) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make caching in the # texmanager more efficient. It is not meant to be used # outside the backend """ if rcParams['text.usetex']: # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) w, h, d = self.get_text_width_height_descent(s, prop, ismath) xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = round(x + xd) y = round(y + yd) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') fname = findfont(prop) font = get_font( fname, hinting_factor=rcParams['text.hinting_factor']) font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points*self.dpi/72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba() def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): """ agg backend doesn't support arbitrary scaling of image. """ return False def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), ... xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy # The incoming data is float, but the _renderer type-checking wants # to see integers. self._renderer.restore_region(region, int(x1), int(y1), int(x2), int(y2), int(ox), int(oy)) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ # WARNING. # For agg_filter to work, the rendere's method need # to overridden in the class. See draw_markers, and draw_path_collections width, height = int(self.width), int(self.height) buffer, bounds = self.tostring_rgba_minimized() l, b, w, h = bounds self._renderer = self._filter_renderers.pop() self._update_methods() if w > 0 and h > 0: img = np.fromstring(buffer, np.uint8) img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., self.dpi) gc = self.new_gc() if img.dtype.kind == 'f': img = np.asarray(img * 255., np.uint8) img = img[::-1] self._renderer.draw_image( gc, l + ox, height - b - h + oy, img)
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 # actual dpi at which we rasterize stuff self._groupd = {} self.basename = basename self._image_counter = itertools.count() self._clipd = OrderedDict() self._markers = {} self._path_collection_id = 0 self._hatchd = OrderedDict() self._has_gouraud = False self._n_gradients = 0 self._fonts = OrderedDict() self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() str_height = short_float_fmt(height) str_width = short_float_fmt(width) svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', width='%spt' % str_width, height='%spt' % str_height, viewBox='0 0 %s %s' % (str_width, str_height), xmlns="http://www.w3.org/2000/svg", version="1.1", attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self.writer.close(self._start_id) self.writer.flush() def _write_default_style(self): writer = self.writer default_style = generate_css({ 'stroke-linejoin': 'round', 'stroke-linecap': 'butt'}) writer.start('defs') writer.start('style', type='text/css') writer.data('*{%s}\n' % default_style) writer.end('style') writer.end('defs') def _make_id(self, type, content): salt = mpl.rcParams['svg.hashsalt'] if salt is None: salt = str(uuid.uuid4()) m = hashlib.md5() m.update(salt.encode('utf8')) m.update(str(content).encode('utf8')) return '%s%s' % (type, m.hexdigest()[:10]) def _make_flip_transform(self, transform): return (transform + Affine2D() .scale(1.0, -1.0) .translate(0.0, self.height)) def _get_font(self, prop): fname = findfont(prop) font = get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ if rgbFace is not None: rgbFace = tuple(rgbFace) edge = gc.get_hatch_color() if edge is not None: edge = tuple(edge) dictkey = (gc.get_hatch(), rgbFace, edge) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id('h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start('defs') for (path, face, stroke), oid in self._hatchd.values(): writer.start( 'pattern', id=oid, patternUnits="userSpaceOnUse", x="0", y="0", width=str(HATCH_SIZE), height=str(HATCH_SIZE)) path_data = self._convert_path( path, Affine2D() .scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if face is None: fill = 'none' else: fill = rgb2hex(face) writer.element( 'rect', x="0", y="0", width=str(HATCH_SIZE+1), height=str(HATCH_SIZE+1), fill=fill) hatch_style = { 'fill': rgb2hex(stroke), 'stroke': rgb2hex(stroke), 'stroke-width': str(mpl.rcParams['hatch.linewidth']), 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter' } if stroke[3] < 1: hatch_style['stroke-opacity'] = str(stroke[3]) writer.element( 'path', d=path_data, style=generate_css(hatch_style) ) writer.end('pattern') writer.end('defs') def _get_style_dict(self, gc, rgbFace): """Generate a style string from the GraphicsContext and rgbFace.""" attrib = {} forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace) if (rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha): attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) else: if rgbFace is None: attrib['fill'] = 'none' else: if tuple(rgbFace[:3]) != (0, 0, 0): attrib['fill'] = rgb2hex(rgbFace) if (len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha): attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) if forced_alpha and gc.get_alpha() != 1.0: attrib['opacity'] = short_float_fmt(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib['stroke-dasharray'] = ','.join( short_float_fmt(val) for val in seq) attrib['stroke-dashoffset'] = short_float_fmt(float(offset)) linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() attrib['stroke'] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: attrib['stroke-opacity'] = short_float_fmt(rgb[3]) if linewidth != 1.0: attrib['stroke-width'] = short_float_fmt(linewidth) if gc.get_joinstyle() != 'round': attrib['stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'butt': attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id('p', dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start('defs') for clip, oid in self._clipd.values(): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path( clippath, clippath_trans, simplify=False) writer.element('path', d=path_data) else: x, y, w, h = clip writer.element( 'rect', x=short_float_fmt(x), y=short_float_fmt(y), width=short_float_fmt(w), height=short_float_fmt(h)) writer.end('clipPath') writer.end('defs') def open_group(self, s, gid=None): # docstring inherited if gid: self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start('g', id="%s_%d" % (s, self._groupd[s])) def close_group(self, s): # docstring inherited self.writer.end('g') def option_image_nocomposite(self): # docstring inherited return not mpl.rcParams['image.composite_image'] def _convert_path(self, path, transform=None, clip=None, simplify=None, sketch=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_string( path, transform, clip, simplify, sketch, 6, [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii') def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path( path, trans_and_flip, clip=clip, simplify=simplify, sketch=gc.get_sketch_params()) attrib = {} attrib['style'] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) self.writer.element('path', d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end('a') def draw_markers( self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited if not len(path.vertices): return writer = self.writer path_data = self._convert_path( marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) style = generate_css({k: v for k, v in style.items() if k.startswith('stroke')}) if oid is None: oid = self._make_id('m', dictkey) writer.start('defs') writer.element('path', id=oid, d=path_data, style=style) writer.end('defs') self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid writer.start('g', attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': '#%s' % oid} clip = (0, 0, self.width*72, self.height*72) for vertices, code in path.iter_segments( trans_and_flip, clip=clip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib['x'] = short_float_fmt(x) attrib['y'] = short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 5) * uses_per_path # cost of definition+use is # (len_path + 3) + 9 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) should_do_optimization = \ len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) writer = self.writer path_codes = [] writer.start('defs') for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = 'C%x_%x_%s' % ( self._path_collection_id, i, self._make_id('', d)) writer.element('path', id=oid, d=d) path_codes.append(oid) writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) if clipid is not None: writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) attrib = { 'xlink:href': '#%s' % path_id, 'x': short_float_fmt(xo), 'y': short_float_fmt(self.height - yo), 'style': self._get_style(gc0, rgbFace) } writer.element('use', attrib=attrib) if clipid is not None: writer.end('g') if url is not None: writer.end('a') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # docstring inherited # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start( 'filter', id='colorAdd') writer.element( 'feComposite', attrib={'in': 'SourceGraphic'}, in2='BackgroundImage', operator='arithmetic', k2="1", k3="1") writer.end('filter') # feColorMatrix filter to correct opacity writer.start( 'filter', id='colorMat') writer.element( 'feColorMatrix', attrib={'type': 'matrix'}, values='1 0 0 0 0 \n0 1 0 0 0 \n0 0 1 0 0' + ' \n1 1 1 1 0 \n0 0 0 0 1 ') writer.end('filter') avg_color = np.average(colors, axis=0) if avg_color[-1] == 0: # Skip fully-transparent triangles return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start('defs') for i in range(3): x1, y1 = tpoints[i] x2, y2 = tpoints[(i + 1) % 3] x3, y3 = tpoints[(i + 2) % 3] rgba_color = colors[i] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start( 'linearGradient', id="GR%x_%d" % (self._n_gradients, i), gradientUnits="userSpaceOnUse", x1=short_float_fmt(x1), y1=short_float_fmt(y1), x2=short_float_fmt(xb), y2=short_float_fmt(yb)) writer.element( 'stop', offset='1', style=generate_css({ 'stop-color': rgb2hex(avg_color), 'stop-opacity': short_float_fmt(rgba_color[-1])})) writer.element( 'stop', offset='0', style=generate_css({'stop-color': rgb2hex(rgba_color), 'stop-opacity': "0"})) writer.end('linearGradient') writer.end('defs') # triangle formation using "path" dpath = "M " + short_float_fmt(x1)+',' + short_float_fmt(y1) dpath += " L " + short_float_fmt(x2) + ',' + short_float_fmt(y2) dpath += " " + short_float_fmt(x3) + ',' + short_float_fmt(y3) + " Z" writer.element( 'path', attrib={'d': dpath, 'fill': rgb2hex(avg_color), 'fill-opacity': '1', 'shape-rendering': "crispEdges"}) writer.start( 'g', attrib={'stroke': "none", 'stroke-width': "0", 'shape-rendering': "crispEdges", 'filter': "url(#colorMat)"}) writer.element( 'path', attrib={'d': dpath, 'fill': 'url(#GR%x_0)' % self._n_gradients, 'shape-rendering': "crispEdges"}) writer.element( 'path', attrib={'d': dpath, 'fill': 'url(#GR%x_1)' % self._n_gradients, 'filter': 'url(#colorAdd)', 'shape-rendering': "crispEdges"}) writer.element( 'path', attrib={'d': dpath, 'fill': 'url(#GR%x_2)' % self._n_gradients, 'filter': 'url(#colorAdd)', 'shape-rendering': "crispEdges"}) writer.end('g') self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib['clip-path'] = 'url(#%s)' % clipid self.writer.start('g', attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end('g') def option_scale_image(self): # docstring inherited return True def get_image_magnification(self): return self.image_dpi / 72.0 def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] if w == 0 or h == 0: return attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) if mpl.rcParams['svg.image_inline']: buf = BytesIO() Image.fromarray(im).save(buf, format="png") oid = oid or self._make_id('image', buf.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(buf.getvalue()).decode('ascii')) else: if self.basename is None: raise ValueError("Cannot save image data to filesystem when " "writing SVG to an in-memory buffer") filename = '{}.image{}.png'.format( self.basename, next(self._image_counter)) _log.info('Writing image file for inclusion: %s', filename) Image.fromarray(im).save(filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename attrib['id'] = oid if transform is None: w = 72.0 * w / self.image_dpi h = 72.0 * h / self.image_dpi self.writer.element( 'image', transform=generate_transform([ ('scale', (1, -1)), ('translate', (0, -h))]), x=short_float_fmt(x), y=short_float_fmt(-(self.height - y - h)), width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = short_float_fmt(alpha) flipped = ( Affine2D().scale(1.0 / w, 1.0 / h) + transform + Affine2D() .translate(x, y) .scale(1.0, -1.0) .translate(0.0, self.height)) attrib['transform'] = generate_transform( [('matrix', flipped.frozen())]) self.writer.element( 'image', width=short_float_fmt(w), height=short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def _update_glyph_map_defs(self, glyph_map_new): """ Emit definitions for not-yet-defined glyphs, and record them as having been defined. """ writer = self.writer if glyph_map_new: writer.start('defs') for char_id, (vertices, codes) in glyph_map_new.items(): char_id = self._adjust_char_id(char_id) path_data = self._convert_path( Path(vertices, codes), simplify=False) writer.element('path', id=char_id, d=path_data) writer.end('defs') self._glyph_map.update(glyph_map_new) def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): """ Draw the text by converting them to paths using the textpath module. Parameters ---------- s : str text to be converted prop : `matplotlib.font_manager.FontProperties` font property ismath : bool If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != '#000000': style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: style['opacity'] = short_float_fmt(alpha) font_scale = fontsize / text2path.FONT_SCALE attrib = { 'style': generate_css(style), 'transform': generate_transform([ ('translate', (x, y)), ('rotate', (-angle,)), ('scale', (font_scale, -font_scale))]), } writer.start('g', attrib=attrib) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: attrib = {'xlink:href': '#%s' % glyph_id} if xposition != 0.0: attrib['x'] = short_float_fmt(xposition) if yposition != 0.0: attrib['y'] = short_float_fmt(yposition) writer.element('use', attrib=attrib) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex( prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext( prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element( 'use', transform=generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), attrib={'xlink:href': '#%s' % char_id}) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element('path', d=path_data) writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != '#000000': style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: style['opacity'] = short_float_fmt(alpha) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) attrib = {} style['font-family'] = str(font.family_name) style['font-weight'] = str(prop.get_weight()).lower() style['font-stretch'] = str(prop.get_stretch()).lower() style['font-style'] = prop.get_style().lower() # Must add "px" to workaround a Firefox bug style['font-size'] = short_float_fmt(prop.get_size()) + 'px' attrib['style'] = generate_css(style) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original # coordinates and add alignment information. # Get anchor coordinates. transform = mtext.get_transform() ax, ay = transform.transform(mtext.get_unitless_position()) ay = self.height - ay # Don't do vertical anchor alignment. Most applications do not # support 'alignment-baseline' yet. Apply the vertical layout # to the anchor point manually for now. angle_rad = np.deg2rad(angle) dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)]) v_offset = np.dot(dir_vert, [(x - ax), (y - ay)]) ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] ha_mpl_to_svg = {'left': 'start', 'right': 'end', 'center': 'middle'} style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] attrib['x'] = short_float_fmt(ax) attrib['y'] = short_float_fmt(ay) attrib['style'] = generate_css(style) attrib['transform'] = "rotate(%s, %s, %s)" % ( short_float_fmt(-angle), short_float_fmt(ax), short_float_fmt(ay)) writer.element('text', s, attrib=attrib) else: attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]) writer.element('text', s, attrib=attrib) else: writer.comment(s) width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib['style'] = generate_css(style) attrib['transform'] = generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]) # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. writer.start('g', attrib=attrib) writer.start('text') # Sort the characters by font, and output one tspan for each. spans = OrderedDict() for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css({ 'font-size': short_float_fmt(fontsize) + 'px', 'font-family': font.family_name, 'font-style': font.style_name.lower(), 'font-weight': font.style_name.lower()}) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) for style, chars in spans.items(): chars.sort() if len({y for x, y, t in chars}) == 1: # Are all y's the same? ys = str(chars[0][1]) else: ys = ' '.join(str(c[1]) for c in chars) attrib = { 'style': style, 'x': ' '.join(short_float_fmt(c[0]) for c in chars), 'y': ys } writer.element( 'tspan', ''.join(chr(c[2]) for c in chars), attrib=attrib) writer.end('text') if len(svg_rects): for x, y, width, height in svg_rects: writer.element( 'rect', x=short_float_fmt(x), y=short_float_fmt(-y + height), width=short_float_fmt(width), height=short_float_fmt(height) ) writer.end('g') def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # docstring inherited self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start( 'g', attrib={'clip-path': 'url(#%s)' % clipid}) if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) if mpl.rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: self.writer.end('a') if clipid is not None: self.writer.end('g') def flipy(self): # docstring inherited return True def get_canvas_width_height(self): # docstring inherited return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited return self._text2path.get_text_width_height_descent(s, prop, ismath)
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 RendererH5Canvas(RendererBase): """The renderer handles drawing/rendering operations.""" fontd = maxdict(50) def __init__(self, width, height, ctx, dpi=72): self.width = width self.height = height self.dpi = dpi self.ctx = ctx self._image_count = 0 # used to uniquely label each image created in this figure... # define the js context self.ctx.width = width self.ctx.height = height #self.ctx.textAlign = "center"; self.ctx.textBaseline = "alphabetic" self.flip = Affine2D().scale(1, -1).translate(0, height) self.mathtext_parser = MathTextParser('bitmap') self._path_time = 0 self._text_time = 0 self._marker_time = 0 self._sub_time = 0 self._last_clip = None self._last_clip_path = None self._clip_count = 0 def _set_style(self, gc, rgbFace=None): ctx = self.ctx if rgbFace is not None: ctx.fillStyle = mpl_to_css_color(rgbFace, gc.get_alpha()) ctx.strokeStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) if gc.get_capstyle(): ctx.lineCap = _capstyle_d[gc.get_capstyle()] ctx.lineWidth = self.points_to_pixels(gc.get_linewidth()) def _path_to_h5(self, ctx, path, transform, clip=None, stroke=True, dashes=(None, None)): """Iterate over a path and produce h5 drawing directives.""" transform = transform + self.flip ctx.beginPath() current_point = None dash_offset, dash_pattern = dashes if dash_pattern is not None: dash_offset = self.points_to_pixels(dash_offset) dash_pattern = tuple( [self.points_to_pixels(dash) for dash in dash_pattern]) for points, code in path.iter_segments(transform, clip=clip): # Shift all points by half a pixel, so that integer coordinates are aligned with pixel centers instead of edges # This prevents lines that are one pixel wide and aligned with the pixel grid from being rendered as a two-pixel wide line # This happens because HTML Canvas defines (0, 0) as the *top left* of a pixel instead of the center, # which causes all integer-valued coordinates to fall exactly between pixels points += 0.5 if code == Path.MOVETO: ctx.moveTo(points[0], points[1]) current_point = (points[0], points[1]) elif code == Path.LINETO: t = time.time() if (dash_pattern is None) or (current_point is None): ctx.lineTo(points[0], points[1]) else: dash_offset = ctx.dashedLine(current_point[0], current_point[1], points[0], points[1], (dash_offset, dash_pattern)) self._sub_time += time.time() - t current_point = (points[0], points[1]) elif code == Path.CURVE3: ctx.quadraticCurveTo(*points) current_point = (points[2], points[3]) elif code == Path.CURVE4: ctx.bezierCurveTo(*points) current_point = (points[4], points[5]) else: pass if stroke: ctx.stroke() def _do_path_clip(self, ctx, clip): self._clip_count += 1 ctx.save() ctx.beginPath() ctx.moveTo(clip[0], clip[1]) ctx.lineTo(clip[2], clip[1]) ctx.lineTo(clip[2], clip[3]) ctx.lineTo(clip[0], clip[3]) ctx.clip() def draw_path(self, gc, path, transform, rgbFace=None): t = time.time() self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) clippath, cliptrans = gc.get_clip_path() ctx = self.ctx if clippath is not None and self._last_clip_path != clippath: ctx.restore() ctx.save() self._path_to_h5(ctx, clippath, cliptrans, None, stroke=False) ctx.clip() self._last_clip_path = clippath if self._last_clip != clip and clip is not None and clippath is None: ctx.restore() self._do_path_clip(ctx, clip) self._last_clip = clip if clip is None and clippath is None and (self._last_clip is not None or self._last_clip_path is not None): self._reset_clip() if rgbFace is None and gc.get_hatch() is None: figure_clip = (0, 0, self.width, self.height) else: figure_clip = None self._path_to_h5(ctx, path, transform, figure_clip, dashes=gc.get_dashes()) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._path_time += time.time() - t def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() if cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) return (x, y, x + w, y + h) return None def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): t = time.time() for vertices, codes in path.iter_segments(trans, simplify=False): if len(vertices): x, y = vertices[-2:] self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) ctx = self.ctx self._path_to_h5(ctx, marker_path, marker_trans + Affine2D().translate(x, y), clip) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._marker_time += time.time() - t def _slipstream_png(self, x, y, im_buffer, width, height): """Insert image directly into HTML canvas as base64-encoded PNG.""" # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) # Write the image into a WebPNG object f = WebPNG() _png.write_png(im_buffer, width, height, f) # Write test PNG as file as well #_png.write_png(im_buffer, width, height, 'canvas_image_%d.png' % (self._image_count,)) # Extract the base64-encoded PNG and send it to the canvas uname = str(uuid.uuid1()).replace( "-", "") #self.ctx._context_name + str(self._image_count) # try to use a unique image name enc = "var canvas_image_%s = 'data:image/png;base64,%s';" % ( uname, f.get_b64()) s = "function imageLoaded_%s(ev) {\nim = ev.target;\nim_left_to_load_%s -=1;\nif (im_left_to_load_%s == 0) frame_body_%s();\n}\ncanv_im_%s = new Image();\ncanv_im_%s.onload = imageLoaded_%s;\ncanv_im_%s.src = canvas_image_%s;\n" % \ (uname, self.ctx._context_name, self.ctx._context_name, self.ctx._context_name, uname, uname, uname, uname, uname) self.ctx.add_header(enc) self.ctx.add_header(s) # Once the base64 encoded image has been received, draw it into the canvas self.ctx.write("%s.drawImage(canv_im_%s, %g, %g, %g, %g);" % (self.ctx._context_name, uname, x, y, width, height)) # draw the image as loaded into canv_im_%d... self._image_count += 1 def _reset_clip(self): self.ctx.restore() self._last_clip = None self._last_clip_path = None #<1.0.0: def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): #1.0.0 and up: def draw_image(self, gc, x, y, im, clippath=None): #API for draw image changed between 0.99 and 1.0.0 def draw_image(self, *args, **kwargs): x, y, im = args[:3] try: h, w = im.get_size_out() except AttributeError: x, y, im = args[1:4] h, w = im.get_size_out() clippath = (kwargs.has_key('clippath') and kwargs['clippath'] or None) if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() if clippath is not None: self._path_to_h5(self.ctx, clippath, clippath_trans, stroke=False) self.ctx.save() self.ctx.clip() (x, y) = self.flip.transform((x, y)) im.flipud_out() rows, cols, im_buffer = im.as_rgba_str() self._slipstream_png(x, (y - h), im_buffer, cols, rows) if clippath is not None: self.ctx.restore() def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() font.set_size(prop.get_size_in_points(), self.dpi) return font def draw_tex(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): logger.error( "Tex support is currently not implemented. Text element '%s' will not be displayed..." % s) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() t = time.time() if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return angle = math.radians(angle) width, height, descent = self.get_text_width_height_descent( s, prop, ismath) x -= math.sin(angle) * descent y -= math.cos(angle) * descent ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) font_size = self.points_to_pixels(prop.get_size_in_points()) font_str = '%s %s %.3gpx %s, %s' % (prop.get_style(), prop.get_weight( ), font_size, prop.get_name(), prop.get_family()[0]) ctx.font = font_str # Set the text color, draw the text and reset the color to black afterwards ctx.fillStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) ctx.fillText(unicode(s), x, y) ctx.fillStyle = '#000000' if angle != 0: ctx.restore() self._text_time = time.time() - t def _draw_mathtext(self, gc, x, y, s, prop, angle): """Draw math text using matplotlib.mathtext.""" # Render math string as an image at the configured DPI, and get the image dimensions and baseline depth rgba, descent = self.mathtext_parser.to_rgba( s, color=gc.get_rgb(), dpi=self.dpi, fontsize=prop.get_size_in_points()) height, width, tmp = rgba.shape angle = math.radians(angle) # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) # Insert math text image into stream, and adjust x, y reference point to be at top left of image self._slipstream_png(x, y - height, rgba.tostring(), width, height) if angle != 0: ctx.restore() def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: image, d = self.mathtext_parser.parse(s, self.dpi, prop) w, h = image.get_width(), image.get_height() else: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() / 64.0 return w, h, d def new_gc(self): return GraphicsContextH5Canvas() def points_to_pixels(self, points): # The standard desktop-publishing (Postscript) point is 1/72 of an inch return points / 72.0 * self.dpi