def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report( 'RendererAgg.__init__ width=%s, height=%s' % (width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying')
def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') #self.draw_path = self._renderer.draw_path # see below self.draw_markers = self._renderer.draw_markers self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.tostring_rgba_minimized = self._renderer.tostring_rgba_minimized self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying')
def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.texd = maxdict(50) # a cache of tex image rasters self._fontd = maxdict(50) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying')
def __init__(self): """ 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 __init__(self): self.mathtext_parser = MathTextParser('path') self.tex_font_map = None from matplotlib.cbook import maxdict self._ps_fontd = maxdict(50) self._texmanager = None self._adobe_standard_encoding = None
def __init__(self, ctx, width, height, dpi, fig): super().__init__() self.fig = fig self.ctx = ctx self.width = width self.height = height self.ctx.width = self.width self.ctx.height = self.height self.dpi = dpi self.fontd = maxdict(50) self.mathtext_parser = MathTextParser("bitmap")
class RendererAgg(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles """ debug=1 # we want to cache the fonts at the class level so that when # multiple figures are created we can reuse them. This helps with # a bug on windows where the creation of too many figures leads to # too many open file handles. However, storing them at the class # level is not thread safe. The solution here is to let the # FigureCanvas acquire a lock on the fontd at the start of the # draw, and release it when it is done. This allows multiple # renderers to share the cached fonts, but only one figure can # draw at at time and so the font cache is used by only one # renderer at a time lock = threading.RLock() _fontd = maxdict(50) def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying') RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height if __debug__: verbose.report('RendererAgg.__init__ width=%s, height=%s'%(width, height), 'debug-annoying') self._renderer = _RendererAgg(int(width), int(height), dpi, debug=False) self._filter_renderers = [] if __debug__: verbose.report('RendererAgg.__init__ _RendererAgg done', 'debug-annoying') self._update_methods() self.mathtext_parser = MathTextParser('Agg') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') def __getstate__(self): # We only want to preserve the init keywords of the Renderer. # Anything else can be re-created. return {'width': self.width, 'height': self.height, 'dpi': self.dpi} def __setstate__(self, state): self.__init__(state['width'], state['height'], state['dpi']) def _get_hinting_flag(self): if rcParams['text.hinting']: return LOAD_FORCE_AUTOHINT else: return LOAD_NO_HINTING # for filtering to work with rasterization, methods needs to be wrapped. # maybe there is better way to do it. def draw_markers(self, *kl, **kw): return self._renderer.draw_markers(*kl, **kw) def draw_path_collection(self, *kl, **kw): return self._renderer.draw_path_collection(*kl, **kw) def _update_methods(self): self.draw_quad_mesh = self._renderer.draw_quad_mesh self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.copy_from_bbox = self._renderer.copy_from_bbox self.get_content_extents = self._renderer.get_content_extents def tostring_rgba_minimized(self): extents = self.get_content_extents() bbox = [[extents[0], self.height - (extents[1] + extents[3])], [extents[0] + extents[2], self.height - extents[1]]] region = self.copy_from_bbox(bbox) return np.array(region), extents def draw_path(self, gc, path, transform, rgbFace=None): """ Draw the path """ nmax = rcParams['agg.path.chunksize'] # here at least for testing npts = path.vertices.shape[0] if (nmax > 100 and npts > nmax and path.should_simplify and rgbFace is None and gc.get_hatch() is None): nch = np.ceil(npts/float(nmax)) chsize = int(np.ceil(npts/nch)) i0 = np.arange(0, npts, chsize) i1 = np.zeros_like(i0) i1[:-1] = i0[1:] - 1 i1[-1] = npts for ii0, ii1 in zip(i0, i1): v = path.vertices[ii0:ii1,:] c = path.codes if c is not None: c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) self._renderer.draw_path(gc, p, transform, rgbFace) else: self._renderer.draw_path(gc, path, transform, rgbFace) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if __debug__: verbose.report('RendererAgg.draw_mathtext', 'debug-annoying') ox, oy, width, height, descent, font_image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) xd = descent * sin(radians(angle)) yd = descent * cos(radians(angle)) x = round(x + ox + xd) y = round(y - oy + yd) self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): """ Render the text """ if __debug__: verbose.report('RendererAgg.draw_text', 'debug-annoying') if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) flags = get_hinting_flag() font = self._get_agg_font(prop) if font is None: return None if len(s) == 1 and ord(s) > 127: font.load_char(ord(s), flags=flags) else: # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). font.set_text(s, 0, flags=flags) font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased']) d = font.get_descent() / 64.0 # The descent needs to be adjusted for the angle xo, yo = font.get_bitmap_offset() xo /= 64.0 yo /= 64.0 xd = -d * sin(radians(angle)) yd = d * cos(radians(angle)) #print x, y, int(x), int(y), s self._renderer.draw_text_image( font, round(x - xd + xo), round(y + yd + yo) + 1, angle, gc) def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop # passing rgb is a little hack to make caching in the # texmanager more efficient. It is not meant to be used # outside the backend """ if rcParams['text.usetex']: # todo: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent flags = get_hinting_flag() font = self._get_agg_font(prop) font.set_text(s, 0.0, flags=flags) # the width and height of unrotated string w, h = font.get_width_height() d = font.get_descent() w /= 64.0 # convert from subpixels h /= 64.0 d /= 64.0 return w, h, d def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) Z = np.array(Z * 255.0, np.uint8) w, h, d = self.get_text_width_height_descent(s, prop, ismath) xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = round(x + xd) y = round(y + yd) self._renderer.draw_text_image(Z, x, y, angle, gc) def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def _get_agg_font(self, prop): """ Get the font for text instance t, cacheing for efficiency """ if __debug__: verbose.report('RendererAgg._get_agg_font', 'debug-annoying') key = hash(prop) font = RendererAgg._fontd.get(key) if font is None: fname = findfont(prop) font = RendererAgg._fontd.get(fname) if font is None: font = FT2Font( fname, hinting_factor=rcParams['text.hinting_factor']) RendererAgg._fontd[fname] = font RendererAgg._fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, self.dpi) return font def points_to_pixels(self, points): """ convert point measures to pixes using dpi and the pixels per inch of the display """ if __debug__: verbose.report('RendererAgg.points_to_pixels', 'debug-annoying') return points*self.dpi/72.0 def tostring_rgb(self): if __debug__: verbose.report('RendererAgg.tostring_rgb', 'debug-annoying') return self._renderer.tostring_rgb() def tostring_argb(self): if __debug__: verbose.report('RendererAgg.tostring_argb', 'debug-annoying') return self._renderer.tostring_argb() def buffer_rgba(self): if __debug__: verbose.report('RendererAgg.buffer_rgba', 'debug-annoying') return self._renderer.buffer_rgba() def clear(self): self._renderer.clear() def option_image_nocomposite(self): # It is generally faster to composite each image directly to # the Figure, and there's no file size benefit to compositing # with the Agg backend return True def option_scale_image(self): """ agg backend support arbitrary scaling of image. """ return True def restore_region(self, region, bbox=None, xy=None): """ Restore the saved region. If bbox (instance of BboxBase, or its extents) is given, only the region specified by the bbox will be restored. *xy* (a tuple of two floasts) optionally specifies the new position (the LLC of the original region, not the LLC of the bbox) where the region will be restored. >>> region = renderer.copy_from_bbox() >>> x1, y1, x2, y2 = region.get_extents() >>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2), ... xy=(x1-dx, y1)) """ if bbox is not None or xy is not None: if bbox is None: x1, y1, x2, y2 = region.get_extents() elif isinstance(bbox, BboxBase): x1, y1, x2, y2 = bbox.extents else: x1, y1, x2, y2 = bbox if xy is None: ox, oy = x1, y1 else: ox, oy = xy # The incoming data is float, but the _renderer type-checking wants # to see integers. self._renderer.restore_region(region, int(x1), int(y1), int(x2), int(y2), int(ox), int(oy)) else: self._renderer.restore_region(region) def start_filter(self): """ Start filtering. It simply create a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), self.dpi) self._update_methods() def stop_filter(self, post_processing): """ Save the plot in the current canvas as a image and apply the *post_processing* function. def post_processing(image, dpi): # ny, nx, depth = image.shape # image (numpy array) has RGBA channels and has a depth of 4. ... # create a new_image (numpy array of 4 channels, size can be # different). The resulting image may have offsets from # lower-left corner of the original image return new_image, offset_x, offset_y The saved renderer is restored and the returned image from post_processing is plotted (using draw_image) on it. """ # WARNING. # For agg_filter to work, the rendere's method need # to overridden in the class. See draw_markers, and draw_path_collections from matplotlib._image import fromarray width, height = int(self.width), int(self.height) buffer, bounds = self.tostring_rgba_minimized() l, b, w, h = bounds self._renderer = self._filter_renderers.pop() self._update_methods() if w > 0 and h > 0: img = np.fromstring(buffer, np.uint8) img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255., self.dpi) image = fromarray(img, 1) gc = self.new_gc() self._renderer.draw_image(gc, l+ox, height - b - h +oy, image)
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self.writer = XMLWriter(svgwriter) self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._has_gouraud = False self._n_gradients = 0 self._fonts = {} self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog) self._start_id = self.writer.start( u'svg', width=u'%ipt' % width, height='%ipt' % height, viewBox=u'0 0 %i %i' % (width, height), xmlns=u"http://www.w3.org/2000/svg", version=u"1.1", attrib={u'xmlns:xlink': u"http://www.w3.org/1999/xlink"}) self._write_default_style() def finalize(self): self._write_clips() self._write_hatches() self._write_svgfonts() self.writer.close(self._start_id) self.writer.flush() def _write_default_style(self): writer = self.writer default_style = generate_css({ u'stroke-linejoin': u'round', u'stroke-linecap': u'square' }) writer.start(u'defs') writer.start(u'style', type=u'text/css') writer.data(u'*{%s}\n' % default_style) writer.end(u'style') writer.end(u'defs') def _make_id(self, type, content): content = str(content) if sys.version_info[0] >= 3: content = content.encode('utf8') return u'%s%s' % (type, md5(content).hexdigest()[:10]) def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ if rgbFace is not None: rgbFace = tuple(rgbFace) edge = gc.get_rgb() if edge is not None: edge = tuple(edge) dictkey = (gc.get_hatch(), rgbFace, edge) oid = self._hatchd.get(dictkey) if oid is None: oid = self._make_id(u'h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid) else: _, oid = oid return oid def _write_hatches(self): if not len(self._hatchd): return HATCH_SIZE = 72 writer = self.writer writer.start('defs') for ((path, face, stroke), oid) in self._hatchd.values(): writer.start(u'pattern', id=oid, patternUnits=u"userSpaceOnUse", x=u"0", y=u"0", width=unicode(HATCH_SIZE), height=unicode(HATCH_SIZE)) path_data = self._convert_path(path, Affine2D().scale(HATCH_SIZE).scale( 1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if face is None: fill = u'none' else: fill = rgb2hex(face) writer.element(u'rect', x=u"0", y=u"0", width=unicode(HATCH_SIZE + 1), height=unicode(HATCH_SIZE + 1), fill=fill) writer.element(u'path', d=path_data, style=generate_css({ u'fill': rgb2hex(stroke), u'stroke': rgb2hex(stroke), u'stroke-width': u'1.0', u'stroke-linecap': u'butt', u'stroke-linejoin': u'miter' })) writer.end(u'pattern') writer.end(u'defs') def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace """ attrib = {} if gc.get_hatch() is not None: attrib[u'fill'] = u"url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: attrib[u'fill'] = u'none' elif tuple(rgbFace[:3]) != (0, 0, 0): attrib[u'fill'] = rgb2hex(rgbFace) if gc.get_alpha() != 1.0: attrib[u'opacity'] = str(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib[u'stroke-dasharray'] = u','.join( [u'%f' % val for val in seq]) attrib[u'stroke-dashoffset'] = unicode(float(offset)) linewidth = gc.get_linewidth() if linewidth: attrib[u'stroke'] = rgb2hex(gc.get_rgb()) if linewidth != 1.0: attrib[u'stroke-width'] = str(linewidth) if gc.get_joinstyle() != 'round': attrib[u'stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'projecting': attrib[u'stroke-linecap'] = _capstyle_d[gc.get_capstyle()] return attrib def _get_style(self, gc, rgbFace): return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) dictkey = (id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) dictkey = (x, y, w, h) else: return None clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id(u'p', dictkey) if clippath is not None: self._clipd[dictkey] = ((clippath, clippath_trans), oid) else: self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip return oid def _write_clips(self): if not len(self._clipd): return writer = self.writer writer.start('defs') for clip, oid in self._clipd.values(): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip path_data = self._convert_path(clippath, clippath_trans, simplify=False) writer.element(u'path', d=path_data) else: x, y, w, h = clip writer.element(u'rect', x=unicode(x), y=unicode(y), width=unicode(w), height=unicode(h)) writer.end(u'clipPath') writer.end(u'defs') def _write_svgfonts(self): if not rcParams['svg.fonttype'] == 'svgfont': return writer = self.writer writer.start(u'defs') for font_fname, chars in self._fonts.items(): font = FT2Font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() writer.start(u'font', id=sfnt[(1, 0, 0, 4)]) writer.element(u'font-face', attrib={ u'font-family': font.family_name, u'font-style': font.style_name.lower(), u'units-per-em': u'72', u'bbox': u' '.join(unicode(x / 64.0) for x in font.bbox) }) for char in chars: glyph = font.load_char(char, flags=LOAD_NO_HINTING) verts, codes = font.get_path() path = Path(verts, codes) path_data = self._convert_path(path) # name = font.get_glyph_name(char) writer.element( u'glyph', d=path_data, attrib={ # 'glyph-name': name, u'unicode': unichr(char), u'horiz-adv-x': unicode(glyph.linearHoriAdvance / 65536.0) }) writer.end(u'font') writer.end(u'defs') def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self.writer.start(u'g', id=u"%s_%d" % (s, self._groupd[s])) def close_group(self, s): self.writer.end('g') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] def _convert_path(self, path, transform=None, clip=None, simplify=None): if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None return _path.convert_to_svg(path, transform, clip, simplify, 6) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify) attrib = {} attrib[u'style'] = self._get_style(gc, rgbFace) clipid = self._get_clip(gc) if clipid is not None: attrib[u'clip-path'] = u'url(#%s)' % clipid if gc.get_url() is not None: self.writer.start(u'a', {u'xlink:href': gc.get_url()}) self.writer.element(u'path', d=path_data, attrib=attrib) if gc.get_url() is not None: self.writer.end(u'a') def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if not len(path.vertices): return writer = self.writer path_data = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) for key in style.keys(): if not key.startswith('stroke'): del style[key] style = generate_css(style) if oid is None: oid = self._make_id(u'm', dictkey) writer.start(u'defs') writer.element(u'path', id=oid, d=path_data, style=style) writer.end(u'defs') self._markers[dictkey] = oid attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib[u'clip-path'] = u'url(#%s)' % clipid writer.start(u'g', attrib=attrib) trans_and_flip = self._make_flip_transform(trans) attrib = {u'xlink:href': u'#%s' % oid} for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] attrib[u'x'] = unicode(x) attrib[u'y'] = unicode(y) attrib[u'style'] = self._get_style(gc, rgbFace) writer.element(u'use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): writer = self.writer path_codes = [] writer.start(u'defs') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) oid = u'C%x_%x_%s' % (self._path_collection_id, i, self._make_id(u'', d)) writer.element(u'path', id=oid, d=d) path_codes.append(oid) writer.end(u'defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start(u'a', attrib={u'xlink:href': url}) if clipid is not None: writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid}) attrib = { u'xlink:href': u'#%s' % path_id, u'x': unicode(xo), u'y': unicode(self.height - yo), u'style': self._get_style(gc0, rgbFace) } writer.element(u'use', attrib=attrib) if clipid is not None: writer.end(u'g') if url is not None: writer.end(u'a') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. writer = self.writer if not self._has_gouraud: self._has_gouraud = True writer.start(u'filter', id=u'colorAdd') writer.element(u'feComposite', attrib={u'in': u'SourceGraphic'}, in2=u'BackgroundImage', operator=u'arithmetic', k2=u"1", k3=u"1") writer.end(u'filter') avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) writer.start(u'defs') for i in range(3): x1, y1 = tpoints[i] x2, y2 = tpoints[(i + 1) % 3] x3, y3 = tpoints[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 writer.start(u'linearGradient', id=u"GR%x_%d" % (self._n_gradients, i), x1=unicode(x1), y1=unicode(y1), x2=unicode(xb), y2=unicode(yb)) writer.element(u'stop', offset=u'0', style=generate_css({ 'stop-color': rgb2hex(c), 'stop-opacity': unicode(c[-1]) })) writer.element(u'stop', offset=u'1', style=generate_css({ u'stop-color': rgb2hex(c), u'stop-opacity': u"0" })) writer.end(u'linearGradient') writer.element(u'polygon', id=u'GT%x' % self._n_gradients, points=u" ".join( [unicode(x) for x in x1, y1, x2, y2, x3, y3])) writer.end('defs') avg_color = np.sum(colors[:, :], axis=0) / 3.0 href = u'#GT%x' % self._n_gradients writer.element(u'use', attrib={ u'xlink:href': href, u'fill': rgb2hex(avg_color), u'fill-opacity': str(avg_color[-1]) }) for i in range(3): writer.element(u'use', attrib={ u'xlink:href': href, u'fill': u'url(#GR%x_%d)' % (self._n_gradients, i), u'fill-opacity': u'1', u'filter': u'url(#colorAdd)' }) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): attrib = {} clipid = self._get_clip(gc) if clipid is not None: attrib[u'clip-path'] = u'url(#%s)' % clipid self.writer.start(u'g', attrib=attrib) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) self.writer.end(u'g') def option_scale_image(self): return True def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): attrib = {} clipid = self._get_clip(gc) if clipid is not None: # Can't apply clip-path directly to the image because the # image has a transformation, which would also be applied # to the clip-path self.writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid}) trans = [1, 0, 0, 1, 0, 0] if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] attrib[u'transform'] = generate_transform([(u'matrix', tuple(trans))]) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() oid = getattr(im, '_gid', None) url = getattr(im, '_url', None) if url is not None: self.writer.start(u'a', attrib={u'xlink:href': url}) if rcParams['svg.image_inline']: bytesio = io.BytesIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, bytesio) im.flipud_out() oid = oid or self._make_id('image', bytesio) attrib['xlink:href'] = ( u"data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode('ascii')) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = u'%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() oid = oid or 'Im_' + self._make_id('image', filename) attrib[u'xlink:href'] = filename alpha = gc.get_alpha() if alpha != 1.0: attrib['opacity'] = str(alpha) attrib['id'] = oid if transform is None: self.writer.element(u'image', x=unicode(x / trans[0]), y=unicode((self.height - y) / trans[3] - h), width=unicode(w), height=unicode(h), attrib=attrib) else: flipped = self._make_flip_transform(transform) flipped = np.array(flipped.to_values()) y = y + dy if dy > 0.0: flipped[3] *= -1.0 y *= -1.0 attrib[u'transform'] = generate_transform([(u'matrix', flipped)]) self.writer.element(u'image', x=unicode(x), y=unicode(y), width=unicode(dx), height=unicode(abs(dy)), attrib=attrib) if url is not None: self.writer.end(u'a') if clipid is not None: self.writer.end(u'g') def _adjust_char_id(self, char_id): return char_id.replace(u"%20", u"_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ writer = self.writer writer.comment(s) glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() style = {} if color != '#000000': style['fill'] = color if gc.get_alpha() != 1.0: style[u'opacity'] = unicode(gc.get_alpha()) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs y -= ((font.get_descent() / 64.0) * (prop.get_size_in_points() / text2path.FONT_SCALE)) if glyph_map_new: writer.start(u'defs') for char_id, glyph_path in glyph_map_new.iteritems(): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element(u'path', id=char_id, d=path_data) writer.end(u'defs') glyph_map.update(glyph_map_new) attrib = {} attrib[u'style'] = generate_css(style) font_scale = fontsize / text2path.FONT_SCALE attrib[u'transform'] = generate_transform([ (u'translate', (x, y)), (u'rotate', (-angle, )), (u'scale', (font_scale, -font_scale)) ]) writer.start(u'g', attrib=attrib) for glyph_id, xposition, yposition, scale in glyph_info: attrib = {u'xlink:href': u'#%s' % glyph_id} if xposition != 0.0: attrib[u'x'] = str(xposition) if yposition != 0.0: attrib[u'y'] = str(yposition) writer.element(u'use', attrib=attrib) writer.end(u'g') else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) else: _glyphs = text2path.get_glyphs_mathtext( prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: writer.start(u'defs') for char_id, glyph_path in glyph_map_new.iteritems(): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): path_data = u"" else: path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element(u'path', id=char_id, d=path_data) writer.end(u'defs') glyph_map.update(glyph_map_new) attrib = {} font_scale = fontsize / text2path.FONT_SCALE attrib[u'style'] = generate_css(style) attrib[u'transform'] = generate_transform([ (u'translate', (x, y)), (u'rotate', (-angle, )), (u'scale', (font_scale, -font_scale)) ]) writer.start(u'g', attrib=attrib) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element(u'use', transform=generate_transform([ (u'translate', (xposition, yposition)), (u'scale', (scale, )), ]), attrib={u'xlink:href': u'#%s' % char_id}) for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, simplify=False) writer.element(u'path', d=path_data) writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath): writer = self.writer color = rgb2hex(gc.get_rgb()) style = {} if color != '#000000': style[u'fill'] = color if gc.get_alpha() != 1.0: style[u'opacity'] = unicode(gc.get_alpha()) if not ismath: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() fontfamily = font.family_name fontstyle = prop.get_style() attrib = {} # Must add "px" to workaround a Firefox bug style[u'font-size'] = str(fontsize) + 'px' style[u'font-family'] = str(fontfamily) style[u'font-style'] = prop.get_style().lower() attrib[u'style'] = generate_css(style) attrib[u'transform'] = generate_transform([(u'translate', (x, y)), (u'rotate', (-angle, )) ]) writer.element(u'text', s, attrib=attrib) if rcParams['svg.fonttype'] == 'svgfont': fontset = self._fonts.setdefault(font.fname, set()) for c in s: fontset.add(ord(c)) else: writer.comment(s) width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects attrib = {} attrib[u'style'] = generate_css(style) attrib[u'transform'] = generate_transform([(u'translate', (x, y)), (u'rotate', (-angle, )) ]) # Apply attributes to 'g', not 'text', because we likely # have some rectangles as well with the same style and # transformation writer.start(u'g', attrib=attrib) writer.start(u'text') # Sort the characters by font, and output one tspan for # each spans = {} for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: style = generate_css({ u'font-size': unicode(fontsize) + 'px', u'font-family': font.family_name, u'font-style': font.style_name.lower() }) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) if rcParams['svg.fonttype'] == 'svgfont': for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) for style, chars in spans.items(): chars.sort() same_y = True if len(chars) > 1: last_y = chars[0][1] for i in xrange(1, len(chars)): if chars[i][1] != last_y: same_y = False break if same_y: ys = unicode(chars[0][1]) else: ys = ' '.join(unicode(c[1]) for c in chars) attrib = { u'style': style, u'x': ' '.join(unicode(c[0]) for c in chars), u'y': ys } writer.element(u'tspan', u''.join(unichr(c[2]) for c in chars), attrib=attrib) writer.end(u'text') if len(svg_rects): for x, y, width, height in svg_rects: writer.element(u'rect', x=unicode(x), y=unicode(-y + height), width=unicode(width), height=unicode(height)) writer.end(u'g') def draw_tex(self, gc, x, y, s, prop, angle): self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath): clipid = self._get_clip(gc) if clipid is not None: # Cannot apply clip-path directly to the text, because # is has a transformation self.writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid}) if rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath) if clipid is not None: self.writer.end(u'g') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): return self._text2path.get_text_width_height_descent(s, prop, ismath)
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
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self.mathtext_parser = MathTextParser('SVG') svgwriter.write(svgProlog % (width, height, width, height)) def _draw_svg_element(self, element, details, gc, rgbFace): cliprect, clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid style = self._get_style(gc, rgbFace) self._svgwriter.write('%s<%s style="%s" %s %s/>\n' % (cliprect, element, style, clippath, details)) def _path_commands(self, path): cmd = [] while 1: code, xp, yp = path.vertex() yp = self.height - yp if code == agg.path_cmd_stop: cmd.append('z') # Hack, path_cmd_end_poly not found break elif code == agg.path_cmd_move_to: cmd.append('M%g %g' % (xp, yp)) elif code == agg.path_cmd_line_to: cmd.append('L%g %g' % (xp, yp)) elif code == agg.path_cmd_curve3: verts = [xp, yp] verts.extend(path.vertex()[1:]) verts[-1] = self.height - verts[-1] cmd.append('Q%g %g %g %g' % tuple(verts)) elif code == agg.path_cmd_curve4: verts = [xp, yp] verts.extend(path.vertex()[1:]) verts[-1] = self.height - verts[-1] verts.extend(path.vertex()[1:]) verts[-1] = self.height - verts[-1] cmd.append('C%g %g %g %g %g %g' % tuple(verts)) elif code == agg.path_cmd_end_poly: cmd.append('z') path_data = "".join(cmd) return path_data def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % ( ','.join(['%f' % val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %f; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % ( fill, rgb2hex(gc.get_rgb()), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %f' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath = gc.get_clip_path() if cliprect is None and clippath is None: return '', None elif clippath is not None: # See if we've already seen this clip rectangle key = hash(clippath) if self._clipd.get(key) is None: # If not, store a new clipPath self._clipd[key] = clippath style = "stroke: gray; fill: none;" path_data = self._path_commands(clippath) path = """\ <defs> <clipPath id="%(key)s"> <path d="%(path_data)s"/> </clipPath> </defs> """ % locals() return path, key else: return '', key elif cliprect is not None: # See if we've already seen this clip rectangle key = hash(cliprect) if self._clipd.get(key) is None: # If not, store a new clipPath self._clipd[key] = cliprect x, y, w, h = cliprect y = self.height - (y + h) style = "stroke: gray; fill: none;" box = """\ <defs> <clipPath id="%(key)s"> <rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f" style="%(style)s"/> </clipPath> </defs> """ % locals() return box, key else: # return id of previously defined clipPath return '', key def open_group(self, s): self._groupd[s] = self._groupd.get(s, 0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def draw_path(self, gc, rgbFace, path): path_data = self._path_commands(path) self._draw_svg_element("path", 'd="%s"' % path_data, gc, rgbFace) def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation): """ Ignores angles for now """ details = 'cx="%f" cy="%f" rx="%f" ry="%f" transform="rotate(%1.1f %f %f)"' % \ (x, self.height-y, width/2.0, height/2.0, -rotation, x, self.height-y) self._draw_svg_element('ellipse', details, gc, rgbFace) def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] def draw_image(self, x, y, im, bbox): trans = [1, 0, 0, 1, 0, 0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) if im.get_interpolation() != 0: trans[4] += trans[0] trans[5] += trans[3] trans[5] = -trans[5] transstr = 'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() if rcParams['svg.image_inline']: filename = os.path.join(tempfile.gettempdir(), tempfile.gettempprefix() + '.png') verbose.report('Writing temporary image file for inlining: %s' % filename) # im.write_png() accepts a filename, not file object, would be # good to avoid using files and write to mem with StringIO # JDH: it *would* be good, but I don't know how to do this # since libpng seems to want a FILE* and StringIO doesn't seem # to provide one. I suspect there is a way, but I don't know # it im.flipud_out() im.write_png(filename) im.flipud_out() imfile = file(filename, 'rb') image64 = base64.encodestring(imfile.read()) imfile.close() os.remove(filename) hrefstr = 'data:image/png;base64,\n' + image64 else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() im.write_png(filename) im.flipud_out() hrefstr = filename self._svgwriter.write( '<image x="%f" y="%f" width="%f" height="%f" ' 'xlink:href="%s" %s/>\n' % (x / trans[0], (self.height - y) / trans[3] - h, w, h, hrefstr, transstr)) def draw_line(self, gc, x1, y1, x2, y2): details = 'd="M%f,%fL%f,%f"' % (x1, self.height - y1, x2, self.height - y2) self._draw_svg_element('path', details, gc, None) def draw_lines(self, gc, x, y, transform=None): if len(x) == 0: return if len(x) != len(y): raise ValueError('x and y must be the same length') y = self.height - y details = ['d="M%f,%f' % (x[0], y[0])] xys = zip(x[1:], y[1:]) details.extend(['L%f,%f' % tup for tup in xys]) details.append('"') details = ''.join(details) self._draw_svg_element('path', details, gc, None) def draw_point(self, gc, x, y): # result seems to have a hole in it... self.draw_arc(gc, gc.get_rgb(), x, y, 1, 0, 0, 0, 0) def draw_polygon(self, gc, rgbFace, points): details = 'points = "%s"' % ' '.join( ['%f,%f' % (x, self.height - y) for x, y in points]) self._draw_svg_element('polygon', details, gc, rgbFace) def draw_rectangle(self, gc, rgbFace, x, y, width, height): details = 'width="%f" height="%f" x="%f" y="%f"' % ( width, height, x, self.height - y - height) self._draw_svg_element('rect', details, gc, rgbFace) def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = [ '<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha()) ] if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append('translate(%f,%f)' % (x, y)) svg.append('scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind currx += kern / 64.0 / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' transform="translate(%f)"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ( 'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f' % (fontsize, fontfamily, fontstyle, color, gc.get_alpha())) if angle != 0: transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % ( x, y, -angle, -x, -y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append( "c%f %f %f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = ''.join(path_data) char_num = 'c_%s' % md5.new(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write style = "fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append( '<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append( ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y)) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x, curr_y = 0.0, 0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = -new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%f,%f) rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%f,%f)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append( '<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self.open_group("mathtext") write(''.join(svg)) self.close_group("mathtext") def finish(self): write = self._svgwriter.write write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
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 Path(object): """ :class:`Path` represents a series of possibly disconnected, possibly closed, line and curve segments. The underlying storage is made up of two parallel numpy arrays: - *vertices*: an Nx2 float array of vertices - *codes*: an N-length uint8 array of vertex types These two arrays always have the same length in the first dimension. For example, to represent a cubic curve, you must provide three vertices as well as three codes ``CURVE3``. The code types are: - ``STOP`` : 1 vertex (ignored) A marker for the end of the entire path (currently not required and ignored) - ``MOVETO`` : 1 vertex Pick up the pen and move to the given vertex. - ``LINETO`` : 1 vertex Draw a line from the current position to the given vertex. - ``CURVE3`` : 1 control point, 1 endpoint Draw a quadratic Bezier curve from the current position, with the given control point, to the given end point. - ``CURVE4`` : 2 control points, 1 endpoint Draw a cubic Bezier curve from the current position, with the given control points, to the given end point. - ``CLOSEPOLY`` : 1 vertex (ignored) Draw a line segment to the start point of the current polyline. Users of Path objects should not access the vertices and codes arrays directly. Instead, they should use :meth:`iter_segments` to get the vertex/code pairs. This is important, since many :class:`Path` objects, as an optimization, do not store a *codes* at all, but have a default one provided for them by :meth:`iter_segments`. Note also that the vertices and codes arrays should be treated as immutable -- there are a number of optimizations and assumptions made up front in the constructor that will not change when the data changes. """ # Path codes STOP = 0 # 1 vertex MOVETO = 1 # 1 vertex LINETO = 2 # 1 vertex CURVE3 = 3 # 2 vertices CURVE4 = 4 # 3 vertices CLOSEPOLY = 0x4f # 1 vertex NUM_VERTICES = [1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] code_type = np.uint8 def __init__(self, vertices, codes=None, _interpolation_steps=1): """ Create a new path with the given vertices and codes. *vertices* is an Nx2 numpy float array, masked array or Python sequence. *codes* is an N-length numpy array or Python sequence of type :attr:`matplotlib.path.Path.code_type`. These two arrays must have the same length in the first dimension. If *codes* is None, *vertices* will be treated as a series of line segments. If *vertices* contains masked values, they will be converted to NaNs which are then handled correctly by the Agg PathIterator and other consumers of path data, such as :meth:`iter_segments`. *interpolation_steps* is used as a hint to certain projections, such as Polar, that this path should be linearly interpolated immediately before drawing. This attribute is primarily an implementation detail and is not intended for public use. """ if ma.isMaskedArray(vertices): vertices = vertices.astype(np.float_).filled(np.nan) else: vertices = np.asarray(vertices, np.float_) if codes is not None: codes = np.asarray(codes, self.code_type) assert codes.ndim == 1 assert len(codes) == len(vertices) if len(codes): assert codes[0] == self.MOVETO assert vertices.ndim == 2 assert vertices.shape[1] == 2 self.should_simplify = ( rcParams['path.simplify'] and (len(vertices) >= 128 and (codes is None or np.all(codes <= Path.LINETO)))) self.simplify_threshold = rcParams['path.simplify_threshold'] self.has_nonfinite = not np.isfinite(vertices).all() self.codes = codes self.vertices = vertices self._interpolation_steps = _interpolation_steps @classmethod def make_compound_path_from_polys(cls, XY): """ (static method) Make a compound path object to draw a number of polygons with equal numbers of sides XY is a (numpolys x numsides x 2) numpy array of vertices. Return object is a :class:`Path` .. plot:: mpl_examples/api/histogram_path_demo.py """ # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for the # CLOSEPOLY; the vert for the closepoly is ignored but we still need # it to keep the codes aligned with the vertices numpolys, numsides, two = XY.shape assert (two == 2) stride = numsides + 1 nverts = numpolys * stride verts = np.zeros((nverts, 2)) codes = np.ones(nverts, int) * cls.LINETO codes[0::stride] = cls.MOVETO codes[numsides::stride] = cls.CLOSEPOLY for i in range(numsides): verts[i::stride] = XY[:, i] return cls(verts, codes) @classmethod def make_compound_path(cls, *args): """ (staticmethod) Make a compound path from a list of Path objects. Only polygons (not curves) are supported. """ for p in args: assert p.codes is None lengths = [len(x) for x in args] total_length = sum(lengths) vertices = np.vstack([x.vertices for x in args]) vertices.reshape((total_length, 2)) codes = cls.LINETO * np.ones(total_length) i = 0 for length in lengths: codes[i] = cls.MOVETO i += length return cls(vertices, codes) def __repr__(self): return "Path(%s, %s)" % (self.vertices, self.codes) def __len__(self): return len(self.vertices) def iter_segments(self, transform=None, remove_nans=True, clip=None, snap=False, stroke_width=1.0, simplify=None, curves=True): """ Iterates over all of the curve segments in the path. Each iteration returns a 2-tuple (*vertices*, *code*), where *vertices* is a sequence of 1 - 3 coordinate pairs, and *code* is one of the :class:`Path` codes. Additionally, this method can provide a number of standard cleanups and conversions to the path. *transform*: if not None, the given affine transformation will be applied to the path. *remove_nans*: if True, will remove all NaNs from the path and insert MOVETO commands to skip over them. *clip*: if not None, must be a four-tuple (x1, y1, x2, y2) defining a rectangle in which to clip the path. *snap*: if None, auto-snap to pixels, to reduce fuzziness of rectilinear lines. If True, force snapping, and if False, don't snap. *stroke_width*: the width of the stroke being drawn. Needed as a hint for the snapping algorithm. *simplify*: if True, perform simplification, to remove vertices that do not affect the appearance of the path. If False, perform no simplification. If None, use the should_simplify member variable. *curves*: If True, curve segments will be returned as curve segments. If False, all curves will be converted to line segments. """ vertices = self.vertices if not len(vertices): return codes = self.codes NUM_VERTICES = self.NUM_VERTICES MOVETO = self.MOVETO LINETO = self.LINETO CLOSEPOLY = self.CLOSEPOLY STOP = self.STOP vertices, codes = cleanup_path(self, transform, remove_nans, clip, snap, stroke_width, simplify, curves) len_vertices = len(vertices) i = 0 while i < len_vertices: code = codes[i] if code == STOP: return else: num_vertices = NUM_VERTICES[int(code) & 0xf] curr_vertices = vertices[i:i + num_vertices].flatten() yield curr_vertices, code i += num_vertices def transformed(self, transform): """ Return a transformed copy of the path. .. seealso:: :class:`matplotlib.transforms.TransformedPath` A specialized path class that will cache the transformed result and automatically update when the transform changes. """ return Path(transform.transform(self.vertices), self.codes, self._interpolation_steps) def contains_point(self, point, transform=None, radius=0.0): """ Returns *True* if the path contains the given point. If *transform* is not *None*, the path will be transformed before performing the test. """ if transform is not None: transform = transform.frozen() result = point_in_path(point[0], point[1], radius, self, transform) return result def contains_path(self, path, transform=None): """ Returns *True* if this path completely contains the given path. If *transform* is not *None*, the path will be transformed before performing the test. """ if transform is not None: transform = transform.frozen() return path_in_path(self, None, path, transform) def get_extents(self, transform=None): """ Returns the extents (*xmin*, *ymin*, *xmax*, *ymax*) of the path. Unlike computing the extents on the *vertices* alone, this algorithm will take into account the curves and deal with control points appropriately. """ from transforms import Bbox path = self if transform is not None: transform = transform.frozen() if not transform.is_affine: path = self.transformed(transform) transform = None return Bbox(get_path_extents(path, transform)) def intersects_path(self, other, filled=True): """ Returns *True* if this path intersects another given path. *filled*, when True, treats the paths as if they were filled. That is, if one path completely encloses the other, :meth:`intersects_path` will return True. """ return path_intersects_path(self, other, filled) def intersects_bbox(self, bbox, filled=True): """ Returns *True* if this path intersects a given :class:`~matplotlib.transforms.Bbox`. *filled*, when True, treats the path as if it was filled. That is, if one path completely encloses the other, :meth:`intersects_path` will return True. """ from transforms import BboxTransformTo rectangle = self.unit_rectangle().transformed(BboxTransformTo(bbox)) result = self.intersects_path(rectangle, filled) return result def interpolated(self, steps): """ Returns a new path resampled to length N x steps. Does not currently handle interpolating curves. """ if steps == 1: return self vertices = simple_linear_interpolation(self.vertices, steps) codes = self.codes if codes is not None: new_codes = Path.LINETO * np.ones(((len(codes) - 1) * steps + 1, )) new_codes[0::steps] = codes else: new_codes = None return Path(vertices, new_codes) def to_polygons(self, transform=None, width=0, height=0): """ Convert this path to a list of polygons. Each polygon is an Nx2 array of vertices. In other words, each polygon has no ``MOVETO`` instructions or curves. This is useful for displaying in backends that do not support compound paths or Bezier curves, such as GDK. If *width* and *height* are both non-zero then the lines will be simplified so that vertices outside of (0, 0), (width, height) will be clipped. """ if len(self.vertices) == 0: return [] if transform is not None: transform = transform.frozen() if self.codes is None and (width == 0 or height == 0): if transform is None: return [self.vertices] else: return [transform.transform(self.vertices)] # Deal with the case where there are curves and/or multiple # subpaths (using extension code) return convert_path_to_polygons(self, transform, width, height) _unit_rectangle = None @classmethod def unit_rectangle(cls): """ (staticmethod) Returns a :class:`Path` of the unit rectangle from (0, 0) to (1, 1). """ if cls._unit_rectangle is None: cls._unit_rectangle = \ cls([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]], [cls.MOVETO, cls.LINETO, cls.LINETO, cls.LINETO, cls.CLOSEPOLY]) return cls._unit_rectangle _unit_regular_polygons = WeakValueDictionary() @classmethod def unit_regular_polygon(cls, numVertices): """ (staticmethod) Returns a :class:`Path` for a unit regular polygon with the given *numVertices* and radius of 1.0, centered at (0, 0). """ if numVertices <= 16: path = cls._unit_regular_polygons.get(numVertices) else: path = None if path is None: theta = (2 * np.pi / numVertices * np.arange(numVertices + 1).reshape((numVertices + 1, 1))) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 verts = np.concatenate((np.cos(theta), np.sin(theta)), 1) codes = np.empty((numVertices + 1, )) codes[0] = cls.MOVETO codes[1:-1] = cls.LINETO codes[-1] = cls.CLOSEPOLY path = cls(verts, codes) if numVertices <= 16: cls._unit_regular_polygons[numVertices] = path return path _unit_regular_stars = WeakValueDictionary() @classmethod def unit_regular_star(cls, numVertices, innerCircle=0.5): """ (staticmethod) Returns a :class:`Path` for a unit regular star with the given numVertices and radius of 1.0, centered at (0, 0). """ if numVertices <= 16: path = cls._unit_regular_stars.get((numVertices, innerCircle)) else: path = None if path is None: ns2 = numVertices * 2 theta = (2 * np.pi / ns2 * np.arange(ns2 + 1)) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 r = np.ones(ns2 + 1) r[1::2] = innerCircle verts = np.vstack( (r * np.cos(theta), r * np.sin(theta))).transpose() codes = np.empty((ns2 + 1, )) codes[0] = cls.MOVETO codes[1:-1] = cls.LINETO codes[-1] = cls.CLOSEPOLY path = cls(verts, codes) if numVertices <= 16: cls._unit_regular_polygons[(numVertices, innerCircle)] = path return path @classmethod def unit_regular_asterisk(cls, numVertices): """ (staticmethod) Returns a :class:`Path` for a unit regular asterisk with the given numVertices and radius of 1.0, centered at (0, 0). """ return cls.unit_regular_star(numVertices, 0.0) _unit_circle = None @classmethod def unit_circle(cls): """ (staticmethod) Returns a :class:`Path` of the unit circle. The circle is approximated using cubic Bezier curves. This uses 8 splines around the circle using the approach presented here: Lancaster, Don. `Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_. """ if cls._unit_circle is None: MAGIC = 0.2652031 SQRTHALF = np.sqrt(0.5) MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0) vertices = np.array([[0.0, -1.0], [MAGIC, -1.0], [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], [SQRTHALF, -SQRTHALF], [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC], [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], [SQRTHALF, SQRTHALF], [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], [MAGIC, 1.0], [0.0, 1.0], [-MAGIC, 1.0], [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45], [-SQRTHALF, SQRTHALF], [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45], [-1.0, MAGIC], [-1.0, 0.0], [-1.0, -MAGIC], [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45], [-SQRTHALF, -SQRTHALF], [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45], [-MAGIC, -1.0], [0.0, -1.0], [0.0, -1.0]], np.float_) codes = cls.CURVE4 * np.ones(26) codes[0] = cls.MOVETO codes[-1] = cls.CLOSEPOLY cls._unit_circle = cls(vertices, codes) return cls._unit_circle _unit_circle_righthalf = None @classmethod def unit_circle_righthalf(cls): """ (staticmethod) Returns a :class:`Path` of the right half of a unit circle. The circle is approximated using cubic Bezier curves. This uses 4 splines around the circle using the approach presented here: Lancaster, Don. `Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_. """ if cls._unit_circle_righthalf is None: MAGIC = 0.2652031 SQRTHALF = np.sqrt(0.5) MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0) vertices = np.array([[0.0, -1.0], [MAGIC, -1.0], [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], [SQRTHALF, -SQRTHALF], [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC], [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], [SQRTHALF, SQRTHALF], [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], [MAGIC, 1.0], [0.0, 1.0], [0.0, -1.0]], np.float_) codes = cls.CURVE4 * np.ones(14) codes[0] = cls.MOVETO codes[-1] = cls.CLOSEPOLY cls._unit_circle_righthalf = cls(vertices, codes) return cls._unit_circle_righthalf @classmethod def arc(cls, theta1, theta2, n=None, is_wedge=False): """ (staticmethod) Returns an arc on the unit circle from angle *theta1* to angle *theta2* (in degrees). If *n* is provided, it is the number of spline segments to make. If *n* is not provided, the number of spline segments is determined based on the delta between *theta1* and *theta2*. Masionobe, L. 2003. `Drawing an elliptical arc using polylines, quadratic or cubic Bezier curves <http://www.spaceroots.org/documents/ellipse/index.html>`_. """ # degrees to radians theta1 *= np.pi / 180.0 theta2 *= np.pi / 180.0 twopi = np.pi * 2.0 halfpi = np.pi * 0.5 eta1 = np.arctan2(np.sin(theta1), np.cos(theta1)) eta2 = np.arctan2(np.sin(theta2), np.cos(theta2)) eta2 -= twopi * np.floor((eta2 - eta1) / twopi) if (theta2 - theta1 > np.pi) and (eta2 - eta1 < np.pi): eta2 += twopi # number of curve segments to make if n is None: n = int(2**np.ceil((eta2 - eta1) / halfpi)) if n < 1: raise ValueError("n must be >= 1 or None") deta = (eta2 - eta1) / n t = np.tan(0.5 * deta) alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0 steps = np.linspace(eta1, eta2, n + 1, True) cos_eta = np.cos(steps) sin_eta = np.sin(steps) xA = cos_eta[:-1] yA = sin_eta[:-1] xA_dot = -yA yA_dot = xA xB = cos_eta[1:] yB = sin_eta[1:] xB_dot = -yB yB_dot = xB if is_wedge: length = n * 3 + 4 vertices = np.empty((length, 2), np.float_) codes = cls.CURVE4 * np.ones((length, ), cls.code_type) vertices[1] = [xA[0], yA[0]] codes[0:2] = [cls.MOVETO, cls.LINETO] codes[-2:] = [cls.LINETO, cls.CLOSEPOLY] vertex_offset = 2 end = length - 2 else: length = n * 3 + 1 vertices = np.empty((length, 2), np.float_) codes = cls.CURVE4 * np.ones((length, ), cls.code_type) vertices[0] = [xA[0], yA[0]] codes[0] = cls.MOVETO vertex_offset = 1 end = length vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot vertices[vertex_offset + 1:end:3, 0] = xB - alpha * xB_dot vertices[vertex_offset + 1:end:3, 1] = yB - alpha * yB_dot vertices[vertex_offset + 2:end:3, 0] = xB vertices[vertex_offset + 2:end:3, 1] = yB return cls(vertices, codes) @classmethod def wedge(cls, theta1, theta2, n=None): """ (staticmethod) Returns a wedge of the unit circle from angle *theta1* to angle *theta2* (in degrees). If *n* is provided, it is the number of spline segments to make. If *n* is not provided, the number of spline segments is determined based on the delta between *theta1* and *theta2*. """ return cls.arc(theta1, theta2, n, True) _hatch_dict = maxdict(8) @classmethod def hatch(cls, hatchpattern, density=6): """ Given a hatch specifier, *hatchpattern*, generates a Path that can be used in a repeated hatching pattern. *density* is the number of lines per unit square. """ from matplotlib.hatch import get_path if hatchpattern is None: return None hatch_path = cls._hatch_dict.get((hatchpattern, density)) if hatch_path is not None: return hatch_path hatch_path = get_path(hatchpattern, density) cls._hatch_dict[(hatchpattern, density)] = hatch_path return hatch_path
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 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 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 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 RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self._hatchd = {} self._n_gradients = 0 self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() svgwriter.write(svgProlog % (width, height, width, height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = u'' else: clippath = u'clip-path="url(#%s)"' % clipid if gc.get_url() is not None: self._svgwriter.write(u'<a xlink:href="%s">' % gc.get_url()) style = self._get_style(gc, rgbFace) self._svgwriter.write(u'<%s style="%s" %s %s/>\n' % (element, style, clippath, details)) if gc.get_url() is not None: self._svgwriter.write(u'</a>') def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_hatch(self, gc, rgbFace): """ Create a new hatch pattern """ HATCH_SIZE = 72 dictkey = (gc.get_hatch(), rgbFace, gc.get_rgb()) id = self._hatchd.get(dictkey) if id is None: id = 'h%s' % md5(unicode(dictkey).encode('ascii')).hexdigest() self._svgwriter.write(u'<defs>\n <pattern id="%s" ' % id) self._svgwriter.write( u'patternUnits="userSpaceOnUse" x="0" y="0" ') self._svgwriter.write(u' width="%d" height="%d" >\n' % (HATCH_SIZE, HATCH_SIZE)) path_data = self._convert_path(gc.get_hatch_path(), Affine2D().scale(HATCH_SIZE).scale( 1.0, -1.0).translate(0, HATCH_SIZE), simplify=False) if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace) self._svgwriter.write( u'<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (HATCH_SIZE + 1, HATCH_SIZE + 1, fill)) path = u'<path d="%s" fill="%s" stroke="%s" stroke-width="1.0"/>' % ( path_data, rgb2hex(gc.get_rgb()), rgb2hex(gc.get_rgb())) self._svgwriter.write(path) self._svgwriter.write(u'\n </pattern>\n</defs>') self._hatchd[dictkey] = id return id def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if gc.get_hatch() is not None: fill = u"url(#%s)" % self._get_hatch(gc, rgbFace) else: if rgbFace is None: fill = u'none' else: fill = rgb2hex(rgbFace) offset, seq = gc.get_dashes() if seq is None: dashes = u'' else: dashes = u'stroke-dasharray: %s; stroke-dashoffset: %f;' % ( u','.join([u'%f' % val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return u'fill: %s; stroke: %s; stroke-width: %f; ' \ u'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f' % ( fill, rgb2hex(gc.get_rgb()), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return u'fill: %s; opacity: %f' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: clippath_trans = self._make_flip_transform(clippath_trans) path_data = self._convert_path(clippath, clippath_trans, simplify=False) path = u'<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) path = u'<rect x="%(x)f" y="%(y)f" width="%(w)f" height="%(h)f"/>' % locals( ) else: return None id = self._clipd.get(path) if id is None: id = u'p%s' % md5(path.encode('ascii')).hexdigest() self._svgwriter.write(u'<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write(u'\n </clipPath>\n</defs>') self._clipd[path] = id return id def open_group(self, s, gid=None): """ Open a grouping element with label *s*. If *gid* is given, use *gid* as the id of the group. """ if gid: self._svgwriter.write(u'<g id="%s">\n' % (gid)) else: self._groupd[s] = self._groupd.get(s, 0) + 1 self._svgwriter.write(u'<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write(u'</g>\n') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] _path_commands = { Path.MOVETO: u'M%f %f', Path.LINETO: u'L%f %f', Path.CURVE3: u'Q%f %f %f %f', Path.CURVE4: u'C%f %f %f %f %f %f' } def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _convert_path(self, path, transform, clip=False, simplify=None): path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 if clip: clip = (0.0, 0.0, self.width, self.height) else: clip = None for points, code in path.iter_segments(transform, clip=clip, simplify=simplify): if code == Path.CLOSEPOLY: segment = u'z' else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender(u"\n") currpos = 0 appender(segment) currpos += len(segment) return u''.join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) clip = (rgbFace is None and gc.get_hatch_path() is None) simplify = path.should_simplify and clip path_data = self._convert_path(path, trans_and_flip, clip=clip, simplify=simplify) self._draw_svg_element(u'path', u'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) name = self._markers.get(key) if name is None: name = u'm%s' % md5(key.encode('ascii')).hexdigest() write(u'<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = u'' else: clippath = u'clip-path="url(#%s)"' % clipid write(u'<g %s>' % clippath) trans_and_flip = self._make_flip_transform(trans) for vertices, code in path.iter_segments(trans_and_flip, simplify=False): if len(vertices): x, y = vertices[-2:] details = u'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write(u'<use style="%s" %s/>\n' % (style, details)) write(u'</g>') def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._svgwriter.write path_codes = [] write(u'<defs>\n') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform, simplify=False) name = u'coll%x_%x_%s' % (self._path_collection_id, i, md5(d.encode('ascii')).hexdigest()) write(u'<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write(u'</defs>\n') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): clipid = self._get_gc_clip_svg(gc0) url = gc0.get_url() if url is not None: self._svgwriter.write(u'<a xlink:href="%s">' % url) if clipid is not None: write(u'<g clip-path="url(#%s)">' % clipid) details = u'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc0, rgbFace) self._svgwriter.write(u'<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write(u'</g>') if url is not None: self._svgwriter.write(u'</a>') self._path_collection_id += 1 def draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html # # that uses three overlapping linear gradients to simulate a # Gouraud triangle. Each gradient goes from fully opaque in # one corner to fully transparent along the opposite edge. # The line between the stop points is perpendicular to the # opposite edge. Underlying these three gradients is a solid # triangle whose color is the average of all three points. avg_color = np.sum(colors[:, :], axis=0) / 3.0 # Just skip fully-transparent triangles if avg_color[-1] == 0.0: return trans_and_flip = self._make_flip_transform(trans) tpoints = trans_and_flip.transform(points) write = self._svgwriter.write write(u'<defs>') for i in range(3): x1, y1 = points[i] x2, y2 = points[(i + 1) % 3] x3, y3 = points[(i + 2) % 3] c = colors[i][:] if x2 == x3: xb = x2 yb = y1 elif y2 == y3: xb = x1 yb = y2 else: m1 = (y2 - y3) / (x2 - x3) b1 = y2 - (m1 * x2) m2 = -(1.0 / m1) b2 = y1 - (m2 * x1) xb = (-b1 + b2) / (m1 - m2) yb = m2 * xb + b2 write( u'<linearGradient id="GR%x_%d" x1="%f" y1="%f" x2="%f" y2="%f" gradientUnits="userSpaceOnUse">' % (self._n_gradients, i, x1, y1, xb, yb)) write(u'<stop offset="0" style="stop-color:%s;stop-opacity:%f"/>' % (rgb2hex(c), c[-1])) write(u'<stop offset="1" style="stop-color:%s;stop-opacity:0"/>' % rgb2hex(c)) write(u'</linearGradient>') # Define the triangle itself as a "def" since we use it 4 times write(u'<polygon id="GT%x" points="%f %f %f %f %f %f"/>' % (self._n_gradients, x1, y1, x2, y2, x3, y3)) write(u'</defs>\n') avg_color = np.sum(colors[:, :], axis=0) / 3.0 write(u'<use xlink:href="#GT%x" fill="%s" fill-opacity="%f"/>\n' % (self._n_gradients, rgb2hex(avg_color), avg_color[-1])) for i in range(3): write( u'<use xlink:href="#GT%x" fill="url(#GR%x_%d)" fill-opacity="1" filter="url(#colorAdd)"/>\n' % (self._n_gradients, self._n_gradients, i)) self._n_gradients += 1 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): write = self._svgwriter.write clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = u'' else: clippath = u'clip-path="url(#%s)"' % clipid write(u'<g %s>\n' % clippath) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): self.draw_gouraud_triangle(gc, tri, col, transform) write(u'</g>\n') def draw_image(self, gc, x, y, im): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = u'' else: clippath = u'clip-path="url(#%s)"' % clipid trans = [1, 0, 0, 1, 0, 0] transstr = u'' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) trans[5] = -trans[5] transstr = u'transform="matrix(%f %f %f %f %f %f)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() url = getattr(im, '_url', None) if url is not None: self._svgwriter.write(u'<a xlink:href="%s">' % url) self._svgwriter.write( u'<image x="%f" y="%f" width="%f" height="%f" ' u'%s %s xlink:href="' % (x / trans[0], (self.height - y) / trans[3] - h, w, h, transstr, clippath)) if rcParams['svg.image_inline']: self._svgwriter.write(u"data:image/png;base64,\n") bytesio = io.BytesIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, bytesio) im.flipud_out() self._svgwriter.write( encodebytes(bytesio.getvalue()).decode('ascii')) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write(u'"/>\n') if url is not None: self._svgwriter.write(u'</a>') def _adjust_char_id(self, char_id): return char_id.replace(u"%20", u"_") def draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ draw the text by converting them to paths using textpath module. *prop* font property *s* text to be converted *usetex* If True, use matplotlib usetex mode. *ismath* If True, use mathtext parser. If "TeX", use *usetex* mode. """ # this method works for normal text, mathtext and usetex mode. # But currently only utilized by draw_tex method. glyph_map = self._glyph_map text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() write = self._svgwriter.write if ismath == False: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs _flip = Affine2D().scale(1.0, -1.0) if glyph_map_new: write(u'<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): path = Path(*glyph_path) path_data = self._convert_path(path, _flip, simplify=False) path_element = u'<path id="%s" d="%s"/>\n' % ( char_id, ''.join(path_data)) write(path_element) write(u'</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append(u'<g clip-path="url(#%s)">\n' % clipid) svg.append(u'<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append(u'translate(%f,%f)' % (x, y)) svg.append(u'scale(%f)">\n' % (fontsize / text2path.FONT_SCALE)) for glyph_id, xposition, yposition, scale in glyph_info: svg.append(u'<use xlink:href="#%s"' % glyph_id) svg.append(u' x="%f" y="%f"' % (xposition, yposition)) #(currx * (self.FONT_SCALE / fontsize))) svg.append(u'/>\n') svg.append(u'</g>\n') if clipid is not None: svg.append(u'</g>\n') svg = u''.join(svg) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map) else: _glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map) glyph_info, glyph_map_new, rects = _glyphs # we store the character glyphs w/o flipping. Instead, the # coordinate will be flipped when this characters are # used. if glyph_map_new: write(u'<defs>\n') for char_id, glyph_path in glyph_map_new.iteritems(): char_id = self._adjust_char_id(char_id) path = Path(*glyph_path) path_data = self._convert_path(path, None, simplify=False) #_flip) path_element = u'<path id="%s" d="%s"/>\n' % ( char_id, ''.join(path_data)) write(path_element) write(u'</defs>\n') glyph_map.update(glyph_map_new) svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append(u'<g clip-path="url(#%s)">\n' % clipid) svg.append(u'<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append(u'translate(%f,%f)' % (x, y)) svg.append(u'scale(%f,-%f)">\n' % (fontsize / text2path.FONT_SCALE, fontsize / text2path.FONT_SCALE)) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) svg.append(u'<use xlink:href="#%s"' % char_id) svg.append(u' x="%f" y="%f" transform="scale(%f)"' % (xposition / scale, yposition / scale, scale)) svg.append(u'/>\n') for verts, codes in rects: path = Path(verts, codes) path_data = self._convert_path(path, None, simplify=False) path_element = u'<path d="%s"/>\n' % (''.join(path_data)) svg.append(path_element) svg.append(u'</g><!-- style -->\n') if clipid is not None: svg.append(u'</g><!-- clipid -->\n') svg = u''.join(svg) write(svg) def draw_tex(self, gc, x, y, s, prop, angle): self.draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, ord(c)) if path is not None: new_chars.append(path) if len(new_chars): write(u'<defs>\n') for path in new_chars: write(path) write(u'</defs>\n') svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append(u'<g clip-path="url(#%s)">\n' % clipid) svg.append(u'<g style="fill: %s; opacity: %f" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append(u'translate(%f,%f)' % (x, y)) svg.append(u'scale(%f)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, ord(c)) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append(u'<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(u' x="%f"' % (currx * (self.FONT_SCALE / fontsize))) svg.append(u'/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append(u'</g>\n') if clipid is not None: svg.append(u'</g>\n') svg = u''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ( u'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %f' % (fontsize, fontfamily, fontstyle, color, gc.get_alpha())) if angle != 0: transform = u'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % ( x, y, -angle, -x, -y) # Inkscape doesn't support rotate(angle x y) else: transform = u'' svg = u"""\ <text style="%(style)s" x="%(x)f" y="%(y)f" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote(u'%s-%d' % (ps_name, char)) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(char, flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append(u"M%f %f" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append(u"l%f %f" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append(u"q%f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append( u"c%f %f %f %f %f %f" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append(u"z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = u''.join(path_data) char_num = u'c_%s' % md5(path_data.encode('ascii')).hexdigest() path_element = u'<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote(u'%s-%d' % (ps_name, char)) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()) write = self._svgwriter.write style = u"fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, char, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, char) if path is not None: new_chars.append(path) if len(new_chars): write(u'<defs>\n') for path in new_chars: write(path) write(u'</defs>\n') svg = [u'<g style="%s" transform="' % style] if angle != 0: svg.append(u'translate(%f,%f)rotate(%1.1f)' % (x, y, -angle)) else: svg.append(u'translate(%f,%f)' % (x, y)) svg.append(u'">\n') for font, fontsize, char, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, char) svg.append( u'<use xlink:href="#%s" transform="translate(%f,%f)scale(%f)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append(u'</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = [u'<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append( u' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y)) # Inkscape doesn't support rotate(angle x y) svg.append(u'>\n') curr_x, curr_y = 0.0, 0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = -new_y_mtc style = u"font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append(u'<tspan style="%s"' % style) xadvance = metrics.advance svg.append(u' textLength="%f"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(u' dx="%f"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(u' dy="%f"' % dy) thetext = escape_xml_text(thetext) svg.append(u'>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append(u'</text>\n') if len(svg_rects): style = u"fill: %s; stroke: none" % color svg.append(u'<g style="%s" transform="' % style) if angle != 0: svg.append(u'translate(%f,%f) rotate(%1.1f)' % (x, y, -angle)) else: svg.append(u'translate(%f,%f)' % (x, y)) svg.append(u'">\n') for x, y, width, height in svg_rects: svg.append( u'<rect x="%f" y="%f" width="%f" height="%f" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append(u"</g>") self.open_group("mathtext") write(u''.join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write(u'</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if rcParams['text.usetex']: size = prop.get_size_in_points() texmanager = self._text2path.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
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 RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forwards the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() self.mathtext_parser = MathTextParser('MacOSX') def set_width_height(self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) if gc != self.gc: n = self.gc.level() - gc.level() for i in range(n): self.gc.restore() self.gc = gc gc.draw_path(path, transform, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) if gc != self.gc: n = self.gc.level() - gc.level() for i in range(n): self.gc.restore() self.gc = gc gc.draw_markers(marker_path, marker_trans, path, trans, rgbFace) def draw_path_collection(self, *args): gc = self.gc args = args[:13] gc.draw_path_collection(*args) def draw_quad_mesh(self, *args): gc = self.gc gc.draw_quad_mesh(*args) def new_gc(self): self.gc.reset() return self.gc def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): im.flipud_out() nrows, ncols, data = im.as_rgba_str() self.gc.draw_image(x, y, nrows, ncols, data, bbox, clippath, clippath_trans) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle): if gc != self.gc: n = self.gc.level() - gc.level() for i in range(n): self.gc.restore() self.gc = gc # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get( key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): if gc != self.gc: n = self.gc.level() - gc.level() for i in range(n): self.gc.restore() self.gc = gc size = prop.get_size_in_points() ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) gc.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if gc != self.gc: n = self.gc.level() - gc.level() for i in range(n): self.gc.restore() self.gc = gc if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() size = prop.get_size_in_points() weight = prop.get_weight() style = prop.get_style() gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': # TODO: handle props size = prop.get_size_in_points() texmanager = self.get_texmanager() Z = texmanager.get_grey(s, size, self.dpi) m, n = Z.shape # TODO: handle descent; This is based on backend_agg.py return n, m, 0 if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent family = prop.get_family() size = prop.get_size_in_points() weight = prop.get_weight() style = prop.get_style() return self.gc.get_text_width_height_descent(unicode(s), family, size, weight, style) def flipy(self): return False def points_to_pixels(self, points): return points / 72.0 * self.dpi def option_image_nocomposite(self): return True
class RendererSVG(RendererBase): FONT_SCALE = 100.0 fontd = maxdict(50) def __init__(self, width, height, svgwriter, basename=None): self.width = width self.height = height self._svgwriter = svgwriter self._groupd = {} if not rcParams['svg.image_inline']: assert basename is not None self.basename = basename self._imaged = {} self._clipd = {} self._char_defs = {} self._markers = {} self._path_collection_id = 0 self._imaged = {} self.mathtext_parser = MathTextParser('SVG') svgwriter.write(svgProlog % (width, height, width, height)) def _draw_svg_element(self, element, details, gc, rgbFace): clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid style = self._get_style(gc, rgbFace) self._svgwriter.write('<%s style="%s" %s %s/>\n' % (element, style, clippath, details)) def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _get_style(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext, rgbFace and clippath """ if rgbFace is None: fill = 'none' else: fill = rgb2hex(rgbFace[:3]) offset, seq = gc.get_dashes() if seq is None: dashes = '' else: dashes = 'stroke-dasharray: %s; stroke-dashoffset: %s;' % ( ','.join(['%s' % val for val in seq]), offset) linewidth = gc.get_linewidth() if linewidth: return 'fill: %s; stroke: %s; stroke-width: %s; ' \ 'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %s' % ( fill, rgb2hex(gc.get_rgb()[:3]), linewidth, gc.get_joinstyle(), _capstyle_d[gc.get_capstyle()], dashes, gc.get_alpha(), ) else: return 'fill: %s; opacity: %s' % (\ fill, gc.get_alpha(), ) def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: path_data = self._convert_path(clippath, clippath_trans) path = '<path d="%s"/>' % path_data elif cliprect is not None: x, y, w, h = cliprect.bounds y = self.height - (y + h) path = '<rect x="%(x)s" y="%(y)s" width="%(w)s" height="%(h)s"/>' % locals( ) else: return None id = self._clipd.get(path) if id is None: id = 'p%s' % md5(path).hexdigest() self._svgwriter.write('<defs>\n <clipPath id="%s">\n' % id) self._svgwriter.write(path) self._svgwriter.write('\n </clipPath>\n</defs>') self._clipd[path] = id return id def open_group(self, s): self._groupd[s] = self._groupd.get(s, 0) + 1 self._svgwriter.write('<g id="%s%d">\n' % (s, self._groupd[s])) def close_group(self, s): self._svgwriter.write('</g>\n') def option_image_nocomposite(self): """ if svg.image_noscale is True, compositing multiple images into one is prohibited """ return rcParams['svg.image_noscale'] _path_commands = { Path.MOVETO: 'M%s %s', Path.LINETO: 'L%s %s', Path.CURVE3: 'Q%s %s %s %s', Path.CURVE4: 'C%s %s %s %s %s %s' } def _make_flip_transform(self, transform): return (transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)) def _convert_path(self, path, transform): tpath = transform.transform_path(path) path_data = [] appender = path_data.append path_commands = self._path_commands currpos = 0 for points, code in tpath.iter_segments(): if code == Path.CLOSEPOLY: segment = 'z' else: segment = path_commands[code] % tuple(points) if currpos + len(segment) > 75: appender("\n") currpos = 0 appender(segment) currpos += len(segment) return ''.join(path_data) def draw_path(self, gc, path, transform, rgbFace=None): trans_and_flip = self._make_flip_transform(transform) path_data = self._convert_path(path, trans_and_flip) self._draw_svg_element('path', 'd="%s"' % path_data, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): write = self._svgwriter.write key = self._convert_path(marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) name = self._markers.get(key) if name is None: name = 'm%s' % md5(key).hexdigest() write('<defs><path id="%s" d="%s"/></defs>\n' % (name, key)) self._markers[key] = name clipid = self._get_gc_clip_svg(gc) if clipid is None: clippath = '' else: clippath = 'clip-path="url(#%s)"' % clipid write('<g %s>' % clippath) trans_and_flip = self._make_flip_transform(trans) tpath = trans_and_flip.transform_path(path) for x, y in tpath.vertices: details = 'xlink:href="#%s" x="%f" y="%f"' % (name, x, y) style = self._get_style(gc, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) write('</g>') def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds): write = self._svgwriter.write path_codes = [] write('<defs>\n') for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0) d = self._convert_path(path, transform) name = 'coll%x_%x_%s' % (self._path_collection_id, i, md5(d).hexdigest()) write('<path id="%s" d="%s"/>\n' % (name, d)) path_codes.append(name) write('</defs>\n') for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds): clipid = self._get_gc_clip_svg(gc) if clipid is not None: write('<g clip-path="url(#%s)">' % clipid) details = 'xlink:href="#%s" x="%f" y="%f"' % (path_id, xo, self.height - yo) style = self._get_style(gc, rgbFace) self._svgwriter.write('<use style="%s" %s/>\n' % (style, details)) if clipid is not None: write('</g>') self._path_collection_id += 1 def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): # MGDTODO: Support clippath here trans = [1, 0, 0, 1, 0, 0] transstr = '' if rcParams['svg.image_noscale']: trans = list(im.get_matrix()) if im.get_interpolation() != 0: trans[4] += trans[0] trans[5] += trans[3] trans[5] = -trans[5] transstr = 'transform="matrix(%s %s %s %s %s %s)" ' % tuple(trans) assert trans[1] == 0 assert trans[2] == 0 numrows, numcols = im.get_size() im.reset_matrix() im.set_interpolation(0) im.resize(numcols, numrows) h, w = im.get_size_out() self._svgwriter.write( '<image x="%s" y="%s" width="%s" height="%s" ' '%s xlink:href="' % (x / trans[0], (self.height - y) / trans[3] - h, w, h, transstr)) if rcParams['svg.image_inline']: self._svgwriter.write("data:image/png;base64,\n") stringio = cStringIO.StringIO() im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, stringio) im.flipud_out() self._svgwriter.write(base64.encodestring(stringio.getvalue())) else: self._imaged[self.basename] = self._imaged.get(self.basename, 0) + 1 filename = '%s.image%d.png' % (self.basename, self._imaged[self.basename]) verbose.report('Writing image file for inclusion: %s' % filename) im.flipud_out() rows, cols, buffer = im.as_rgba_str() _png.write_png(buffer, cols, rows, filename) im.flipud_out() self._svgwriter.write(filename) self._svgwriter.write('"/>\n') def draw_text(self, gc, x, y, s, prop, angle, ismath): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) y -= font.get_descent() / 64.0 fontsize = prop.get_size_in_points() color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write if rcParams['svg.embed_char_paths']: new_chars = [] for c in s: path = self._add_char_def(prop, c) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = [] clipid = self._get_gc_clip_svg(gc) if clipid is not None: svg.append('<g clip-path="url(#%s)">\n' % clipid) svg.append('<g style="fill: %s; opacity: %s" transform="' % (color, gc.get_alpha())) if angle != 0: svg.append('translate(%s,%s)rotate(%1.1f)' % (x, y, -angle)) elif x != 0 or y != 0: svg.append('translate(%s,%s)' % (x, y)) svg.append('scale(%s)">\n' % (fontsize / self.FONT_SCALE)) cmap = font.get_charmap() lastgind = None currx = 0 for c in s: charnum = self._get_char_def_id(prop, c) ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') gind = 0 glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 currx += (kern / 64.0) / (self.FONT_SCALE / fontsize) svg.append('<use xlink:href="#%s"' % charnum) if currx != 0: svg.append(' x="%s"' % (currx * (self.FONT_SCALE / fontsize))) svg.append('/>\n') currx += (glyph.linearHoriAdvance / 65536.0) / (self.FONT_SCALE / fontsize) lastgind = gind svg.append('</g>\n') if clipid is not None: svg.append('</g>\n') svg = ''.join(svg) else: thetext = escape_xml_text(s) fontfamily = font.family_name fontstyle = prop.get_style() style = ( 'font-size: %f; font-family: %s; font-style: %s; fill: %s; opacity: %s' % (fontsize, fontfamily, fontstyle, color, gc.get_alpha())) if angle != 0: transform = 'transform="translate(%s,%s) rotate(%1.1f) translate(%s,%s)"' % ( x, y, -angle, -x, -y) # Inkscape doesn't support rotate(angle x y) else: transform = '' svg = """\ <text style="%(style)s" x="%(x)s" y="%(y)s" %(transform)s>%(thetext)s</text> """ % locals() write(svg) def _add_char_def(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) char_num = self._char_defs.get(char_id, None) if char_num is not None: return None path_data = [] glyph = font.load_char(ord(char), flags=LOAD_NO_HINTING) currx, curry = 0.0, 0.0 for step in glyph.path: if step[0] == 0: # MOVE_TO path_data.append("M%s %s" % (step[1], -step[2])) elif step[0] == 1: # LINE_TO path_data.append("l%s %s" % (step[1] - currx, -step[2] - curry)) elif step[0] == 2: # CURVE3 path_data.append("q%s %s %s %s" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry)) elif step[0] == 3: # CURVE4 path_data.append( "c%s %s %s %s %s %s" % (step[1] - currx, -step[2] - curry, step[3] - currx, -step[4] - curry, step[5] - currx, -step[6] - curry)) elif step[0] == 4: # ENDPOLY path_data.append("z") currx, curry = 0.0, 0.0 if step[0] != 4: currx, curry = step[-2], -step[-1] path_data = ''.join(path_data) char_num = 'c_%s' % md5(path_data).hexdigest() path_element = '<path id="%s" d="%s"/>\n' % (char_num, ''.join(path_data)) self._char_defs[char_id] = char_num return path_element def _get_char_def_id(self, prop, char): if isinstance(prop, FontProperties): newprop = prop.copy() font = self._get_font(newprop) else: font = prop font.set_size(self.FONT_SCALE, 72) ps_name = font.get_sfnt()[(1, 0, 0, 6)] char_id = urllib.quote('%s-%d' % (ps_name, ord(char))) return self._char_defs[char_id] def _draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw math text using matplotlib.mathtext """ width, height, descent, svg_elements, used_characters = \ self.mathtext_parser.parse(s, 72, prop) svg_glyphs = svg_elements.svg_glyphs svg_rects = svg_elements.svg_rects color = rgb2hex(gc.get_rgb()[:3]) write = self._svgwriter.write style = "fill: %s" % color if rcParams['svg.embed_char_paths']: new_chars = [] for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: path = self._add_char_def(font, thetext) if path is not None: new_chars.append(path) if len(new_chars): write('<defs>\n') for path in new_chars: write(path) write('</defs>\n') svg = ['<g style="%s" transform="' % style] if angle != 0: svg.append('translate(%s,%s)rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%s,%s)' % (x, y)) svg.append('">\n') for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: charid = self._get_char_def_id(font, thetext) svg.append( '<use xlink:href="#%s" transform="translate(%s,%s)scale(%s)"/>\n' % (charid, new_x, -new_y_mtc, fontsize / self.FONT_SCALE)) svg.append('</g>\n') else: # not rcParams['svg.embed_char_paths'] svg = ['<text style="%s" x="%f" y="%f"' % (style, x, y)] if angle != 0: svg.append( ' transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x, y, -angle, -x, -y)) # Inkscape doesn't support rotate(angle x y) svg.append('>\n') curr_x, curr_y = 0.0, 0.0 for font, fontsize, thetext, new_x, new_y_mtc, metrics in svg_glyphs: new_y = -new_y_mtc style = "font-size: %f; font-family: %s" % (fontsize, font.family_name) svg.append('<tspan style="%s"' % style) xadvance = metrics.advance svg.append(' textLength="%s"' % xadvance) dx = new_x - curr_x if dx != 0.0: svg.append(' dx="%s"' % dx) dy = new_y - curr_y if dy != 0.0: svg.append(' dy="%s"' % dy) thetext = escape_xml_text(thetext) svg.append('>%s</tspan>\n' % thetext) curr_x = new_x + xadvance curr_y = new_y svg.append('</text>\n') if len(svg_rects): style = "fill: %s; stroke: none" % color svg.append('<g style="%s" transform="' % style) if angle != 0: svg.append('translate(%s,%s) rotate(%1.1f)' % (x, y, -angle)) else: svg.append('translate(%s,%s)' % (x, y)) svg.append('">\n') for x, y, width, height in svg_rects: svg.append( '<rect x="%s" y="%s" width="%s" height="%s" fill="black" stroke="none" />' % (x, -y + height, width, height)) svg.append("</g>") self.open_group("mathtext") write(''.join(svg)) self.close_group("mathtext") def finalize(self): write = self._svgwriter.write write('</svg>\n') def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: width, height, descent, trash, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 return w, h, d
class Path(object): """ :class:`Path` represents a series of possibly disconnected, possibly closed, line and curve segments. The underlying storage is made up of two parallel numpy arrays: - *vertices*: an Nx2 float array of vertices - *codes*: an N-length uint8 array of vertex types These two arrays always have the same length in the first dimension. For example, to represent a cubic curve, you must provide three vertices as well as three codes ``CURVE3``. The code types are: - ``STOP`` : 1 vertex (ignored) A marker for the end of the entire path (currently not required and ignored) - ``MOVETO`` : 1 vertex Pick up the pen and move to the given vertex. - ``LINETO`` : 1 vertex Draw a line from the current position to the given vertex. - ``CURVE3`` : 1 control point, 1 endpoint Draw a quadratic Bezier curve from the current position, with the given control point, to the given end point. - ``CURVE4`` : 2 control points, 1 endpoint Draw a cubic Bezier curve from the current position, with the given control points, to the given end point. - ``CLOSEPOLY`` : 1 vertex (ignored) Draw a line segment to the start point of the current polyline. Users of Path objects should not access the vertices and codes arrays directly. Instead, they should use :meth:`iter_segments` or :meth:`cleaned` to get the vertex/code pairs. This is important, since many :class:`Path` objects, as an optimization, do not store a *codes* at all, but have a default one provided for them by :meth:`iter_segments`. .. note:: The vertices and codes arrays should be treated as immutable -- there are a number of optimizations and assumptions made up front in the constructor that will not change when the data changes. """ # Path codes STOP = 0 # 1 vertex MOVETO = 1 # 1 vertex LINETO = 2 # 1 vertex CURVE3 = 3 # 2 vertices CURVE4 = 4 # 3 vertices CLOSEPOLY = 79 # 1 vertex #: A dictionary mapping Path codes to the number of vertices that the #: code expects. NUM_VERTICES_FOR_CODE = { STOP: 1, MOVETO: 1, LINETO: 1, CURVE3: 2, CURVE4: 3, CLOSEPOLY: 1 } code_type = np.uint8 def __init__(self, vertices, codes=None, _interpolation_steps=1, closed=False, readonly=False): """ Create a new path with the given vertices and codes. Parameters ---------- vertices : array_like The ``(n, 2)`` float array, masked array or sequence of pairs representing the vertices of the path. If *vertices* contains masked values, they will be converted to NaNs which are then handled correctly by the Agg PathIterator and other consumers of path data, such as :meth:`iter_segments`. codes : {None, array_like}, optional n-length array integers representing the codes of the path. If not None, codes must be the same length as vertices. If None, *vertices* will be treated as a series of line segments. _interpolation_steps : int, optional Used as a hint to certain projections, such as Polar, that this path should be linearly interpolated immediately before drawing. This attribute is primarily an implementation detail and is not intended for public use. closed : bool, optional If *codes* is None and closed is True, vertices will be treated as line segments of a closed polygon. readonly : bool, optional Makes the path behave in an immutable way and sets the vertices and codes as read-only arrays. """ if isinstance(vertices, np.ma.MaskedArray): vertices = vertices.astype(float).filled(np.nan) else: vertices = np.asarray(vertices, float) if (vertices.ndim != 2) or (vertices.shape[1] != 2): msg = "'vertices' must be a 2D list or array with shape Nx2" raise ValueError(msg) if codes is not None: codes = np.asarray(codes, self.code_type) if (codes.ndim != 1) or len(codes) != len(vertices): msg = ("'codes' must be a 1D list or array with the same" " length of 'vertices'") raise ValueError(msg) if len(codes) and codes[0] != self.MOVETO: msg = ("The first element of 'code' must be equal to 'MOVETO':" " {0}") raise ValueError(msg.format(self.MOVETO)) elif closed: codes = np.empty(len(vertices), dtype=self.code_type) codes[0] = self.MOVETO codes[1:-1] = self.LINETO codes[-1] = self.CLOSEPOLY self._vertices = vertices self._codes = codes self._interpolation_steps = _interpolation_steps self._update_values() if readonly: self._vertices.flags.writeable = False if self._codes is not None: self._codes.flags.writeable = False self._readonly = True else: self._readonly = False @classmethod def _fast_from_codes_and_verts(cls, verts, codes, internals=None): """ Creates a Path instance without the expense of calling the constructor Parameters ---------- verts : numpy array codes : numpy array internals : dict or None The attributes that the resulting path should have. Allowed keys are ``readonly``, ``should_simplify``, ``simplify_threshold``, ``has_nonfinite`` and ``interpolation_steps``. """ internals = internals or {} pth = cls.__new__(cls) if isinstance(verts, np.ma.MaskedArray): verts = verts.astype(float).filled(np.nan) else: verts = np.asarray(verts, float) pth._vertices = verts pth._codes = codes pth._readonly = internals.pop('readonly', False) pth.should_simplify = internals.pop('should_simplify', True) pth.simplify_threshold = (internals.pop( 'simplify_threshold', rcParams['path.simplify_threshold'])) pth._has_nonfinite = internals.pop('has_nonfinite', False) pth._interpolation_steps = internals.pop('interpolation_steps', 1) if internals: raise ValueError('Unexpected internals provided to ' '_fast_from_codes_and_verts: ' '{0}'.format('\n *'.join( six.iterkeys(internals)))) return pth def _update_values(self): self._should_simplify = ( rcParams['path.simplify'] and (len(self._vertices) >= 128 and (self._codes is None or np.all(self._codes <= Path.LINETO)))) self._simplify_threshold = rcParams['path.simplify_threshold'] self._has_nonfinite = not np.isfinite(self._vertices).all() @property def vertices(self): """ The list of vertices in the `Path` as an Nx2 numpy array. """ return self._vertices @vertices.setter def vertices(self, vertices): if self._readonly: raise AttributeError("Can't set vertices on a readonly Path") self._vertices = vertices self._update_values() @property def codes(self): """ The list of codes in the `Path` as a 1-D numpy array. Each code is one of `STOP`, `MOVETO`, `LINETO`, `CURVE3`, `CURVE4` or `CLOSEPOLY`. For codes that correspond to more than one vertex (`CURVE3` and `CURVE4`), that code will be repeated so that the length of `self.vertices` and `self.codes` is always the same. """ return self._codes @codes.setter def codes(self, codes): if self._readonly: raise AttributeError("Can't set codes on a readonly Path") self._codes = codes self._update_values() @property def simplify_threshold(self): """ The fraction of a pixel difference below which vertices will be simplified out. """ return self._simplify_threshold @simplify_threshold.setter def simplify_threshold(self, threshold): self._simplify_threshold = threshold @property def has_nonfinite(self): """ `True` if the vertices array has nonfinite values. """ return self._has_nonfinite @property def should_simplify(self): """ `True` if the vertices array should be simplified. """ return self._should_simplify @should_simplify.setter def should_simplify(self, should_simplify): self._should_simplify = should_simplify @property def readonly(self): """ `True` if the `Path` is read-only. """ return self._readonly def __copy__(self): """ Returns a shallow copy of the `Path`, which will share the vertices and codes with the source `Path`. """ import copy return copy.copy(self) copy = __copy__ def __deepcopy__(self, memo=None): """ Returns a deepcopy of the `Path`. The `Path` will not be readonly, even if the source `Path` is. """ try: codes = self.codes.copy() except AttributeError: codes = None return self.__class__(self.vertices.copy(), codes, _interpolation_steps=self._interpolation_steps) deepcopy = __deepcopy__ @classmethod def make_compound_path_from_polys(cls, XY): """ Make a compound path object to draw a number of polygons with equal numbers of sides XY is a (numpolys x numsides x 2) numpy array of vertices. Return object is a :class:`Path` .. plot:: mpl_examples/api/histogram_path_demo.py """ # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for # the CLOSEPOLY; the vert for the closepoly is ignored but we still # need it to keep the codes aligned with the vertices numpolys, numsides, two = XY.shape if two != 2: raise ValueError("The third dimension of 'XY' must be 2") stride = numsides + 1 nverts = numpolys * stride verts = np.zeros((nverts, 2)) codes = np.ones(nverts, int) * cls.LINETO codes[0::stride] = cls.MOVETO codes[numsides::stride] = cls.CLOSEPOLY for i in range(numsides): verts[i::stride] = XY[:, i] return cls(verts, codes) @classmethod def make_compound_path(cls, *args): """Make a compound path from a list of Path objects.""" # Handle an empty list in args (i.e. no args). if not args: return Path(np.empty([0, 2], dtype=np.float32)) lengths = [len(x) for x in args] total_length = sum(lengths) vertices = np.vstack([x.vertices for x in args]) vertices.reshape((total_length, 2)) codes = np.empty(total_length, dtype=cls.code_type) i = 0 for path in args: if path.codes is None: codes[i] = cls.MOVETO codes[i + 1:i + len(path.vertices)] = cls.LINETO else: codes[i:i + len(path.codes)] = path.codes i += len(path.vertices) return cls(vertices, codes) def __repr__(self): return "Path(%r, %r)" % (self.vertices, self.codes) def __len__(self): return len(self.vertices) def iter_segments(self, transform=None, remove_nans=True, clip=None, snap=False, stroke_width=1.0, simplify=None, curves=True, sketch=None): """ Iterates over all of the curve segments in the path. Each iteration returns a 2-tuple (*vertices*, *code*), where *vertices* is a sequence of 1 - 3 coordinate pairs, and *code* is one of the :class:`Path` codes. Additionally, this method can provide a number of standard cleanups and conversions to the path. Parameters ---------- transform : None or :class:`~matplotlib.transforms.Transform` instance If not None, the given affine transformation will be applied to the path. remove_nans : {False, True}, optional If True, will remove all NaNs from the path and insert MOVETO commands to skip over them. clip : None or sequence, optional If not None, must be a four-tuple (x1, y1, x2, y2) defining a rectangle in which to clip the path. snap : None or bool, optional If None, auto-snap to pixels, to reduce fuzziness of rectilinear lines. If True, force snapping, and if False, don't snap. stroke_width : float, optional The width of the stroke being drawn. Needed as a hint for the snapping algorithm. simplify : None or bool, optional If True, perform simplification, to remove vertices that do not affect the appearance of the path. If False, perform no simplification. If None, use the should_simplify member variable. curves : {True, False}, optional If True, curve segments will be returned as curve segments. If False, all curves will be converted to line segments. sketch : None or sequence, optional If not None, must be a 3-tuple of the form (scale, length, randomness), representing the sketch parameters. """ if not len(self): return cleaned = self.cleaned(transform=transform, remove_nans=remove_nans, clip=clip, snap=snap, stroke_width=stroke_width, simplify=simplify, curves=curves, sketch=sketch) vertices = cleaned.vertices codes = cleaned.codes len_vertices = vertices.shape[0] # Cache these object lookups for performance in the loop. NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE STOP = self.STOP i = 0 while i < len_vertices: code = codes[i] if code == STOP: return else: num_vertices = NUM_VERTICES_FOR_CODE[code] curr_vertices = vertices[i:i + num_vertices].flatten() yield curr_vertices, code i += num_vertices def cleaned(self, transform=None, remove_nans=False, clip=None, quantize=False, simplify=False, curves=False, stroke_width=1.0, snap=False, sketch=None): """ Cleans up the path according to the parameters returning a new Path instance. .. seealso:: See :meth:`iter_segments` for details of the keyword arguments. Returns ------- Path instance with cleaned up vertices and codes. """ vertices, codes = _path.cleanup_path(self, transform, remove_nans, clip, snap, stroke_width, simplify, curves, sketch) internals = { 'should_simplify': self.should_simplify and not simplify, 'has_nonfinite': self.has_nonfinite and not remove_nans, 'simplify_threshold': self.simplify_threshold, 'interpolation_steps': self._interpolation_steps } return Path._fast_from_codes_and_verts(vertices, codes, internals) def transformed(self, transform): """ Return a transformed copy of the path. .. seealso:: :class:`matplotlib.transforms.TransformedPath` A specialized path class that will cache the transformed result and automatically update when the transform changes. """ return Path(transform.transform(self.vertices), self.codes, self._interpolation_steps) def contains_point(self, point, transform=None, radius=0.0): """ Returns *True* if the path contains the given point. If *transform* is not *None*, the path will be transformed before performing the test. *radius* allows the path to be made slightly larger or smaller. """ if transform is not None: transform = transform.frozen() result = _path.point_in_path(point[0], point[1], radius, self, transform) return result def contains_points(self, points, transform=None, radius=0.0): """ Returns a bool array which is *True* if the path contains the corresponding point. If *transform* is not *None*, the path will be transformed before performing the test. *radius* allows the path to be made slightly larger or smaller. """ if transform is not None: transform = transform.frozen() result = _path.points_in_path(points, radius, self, transform) return result.astype('bool') def contains_path(self, path, transform=None): """ Returns *True* if this path completely contains the given path. If *transform* is not *None*, the path will be transformed before performing the test. """ if transform is not None: transform = transform.frozen() return _path.path_in_path(self, None, path, transform) def get_extents(self, transform=None): """ Returns the extents (*xmin*, *ymin*, *xmax*, *ymax*) of the path. Unlike computing the extents on the *vertices* alone, this algorithm will take into account the curves and deal with control points appropriately. """ from .transforms import Bbox path = self if transform is not None: transform = transform.frozen() if not transform.is_affine: path = self.transformed(transform) transform = None return Bbox(_path.get_path_extents(path, transform)) def intersects_path(self, other, filled=True): """ Returns *True* if this path intersects another given path. *filled*, when True, treats the paths as if they were filled. That is, if one path completely encloses the other, :meth:`intersects_path` will return True. """ return _path.path_intersects_path(self, other, filled) def intersects_bbox(self, bbox, filled=True): """ Returns *True* if this path intersects a given :class:`~matplotlib.transforms.Bbox`. *filled*, when True, treats the path as if it was filled. That is, if one path completely encloses the other, :meth:`intersects_path` will return True. """ from .transforms import BboxTransformTo rectangle = self.unit_rectangle().transformed(BboxTransformTo(bbox)) result = self.intersects_path(rectangle, filled) return result def interpolated(self, steps): """ Returns a new path resampled to length N x steps. Does not currently handle interpolating curves. """ if steps == 1: return self vertices = simple_linear_interpolation(self.vertices, steps) codes = self.codes if codes is not None: new_codes = Path.LINETO * np.ones(((len(codes) - 1) * steps + 1, )) new_codes[0::steps] = codes else: new_codes = None return Path(vertices, new_codes) def to_polygons(self, transform=None, width=0, height=0, closed_only=True): """ Convert this path to a list of polygons or polylines. Each polygon/polyline is an Nx2 array of vertices. In other words, each polygon has no ``MOVETO`` instructions or curves. This is useful for displaying in backends that do not support compound paths or Bezier curves, such as GDK. If *width* and *height* are both non-zero then the lines will be simplified so that vertices outside of (0, 0), (width, height) will be clipped. If *closed_only* is `True` (default), only closed polygons, with the last point being the same as the first point, will be returned. Any unclosed polylines in the path will be explicitly closed. If *closed_only* is `False`, any unclosed polygons in the path will be returned as unclosed polygons, and the closed polygons will be returned explicitly closed by setting the last point to the same as the first point. """ if len(self.vertices) == 0: return [] if transform is not None: transform = transform.frozen() if self.codes is None and (width == 0 or height == 0): vertices = self.vertices if closed_only: if len(vertices) < 3: return [] elif np.any(vertices[0] != vertices[-1]): vertices = list(vertices) + [vertices[0]] if transform is None: return [vertices] else: return [transform.transform(vertices)] # Deal with the case where there are curves and/or multiple # subpaths (using extension code) return _path.convert_path_to_polygons(self, transform, width, height, closed_only) _unit_rectangle = None @classmethod def unit_rectangle(cls): """ Return a :class:`Path` instance of the unit rectangle from (0, 0) to (1, 1). """ if cls._unit_rectangle is None: cls._unit_rectangle = \ cls([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]], [cls.MOVETO, cls.LINETO, cls.LINETO, cls.LINETO, cls.CLOSEPOLY], readonly=True) return cls._unit_rectangle _unit_regular_polygons = WeakValueDictionary() @classmethod def unit_regular_polygon(cls, numVertices): """ Return a :class:`Path` instance for a unit regular polygon with the given *numVertices* and radius of 1.0, centered at (0, 0). """ if numVertices <= 16: path = cls._unit_regular_polygons.get(numVertices) else: path = None if path is None: theta = (2 * np.pi / numVertices * np.arange(numVertices + 1).reshape((numVertices + 1, 1))) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 verts = np.concatenate((np.cos(theta), np.sin(theta)), 1) codes = np.empty((numVertices + 1, )) codes[0] = cls.MOVETO codes[1:-1] = cls.LINETO codes[-1] = cls.CLOSEPOLY path = cls(verts, codes, readonly=True) if numVertices <= 16: cls._unit_regular_polygons[numVertices] = path return path _unit_regular_stars = WeakValueDictionary() @classmethod def unit_regular_star(cls, numVertices, innerCircle=0.5): """ Return a :class:`Path` for a unit regular star with the given numVertices and radius of 1.0, centered at (0, 0). """ if numVertices <= 16: path = cls._unit_regular_stars.get((numVertices, innerCircle)) else: path = None if path is None: ns2 = numVertices * 2 theta = (2 * np.pi / ns2 * np.arange(ns2 + 1)) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 r = np.ones(ns2 + 1) r[1::2] = innerCircle verts = np.vstack( (r * np.cos(theta), r * np.sin(theta))).transpose() codes = np.empty((ns2 + 1, )) codes[0] = cls.MOVETO codes[1:-1] = cls.LINETO codes[-1] = cls.CLOSEPOLY path = cls(verts, codes, readonly=True) if numVertices <= 16: cls._unit_regular_stars[(numVertices, innerCircle)] = path return path @classmethod def unit_regular_asterisk(cls, numVertices): """ Return a :class:`Path` for a unit regular asterisk with the given numVertices and radius of 1.0, centered at (0, 0). """ return cls.unit_regular_star(numVertices, 0.0) _unit_circle = None @classmethod def unit_circle(cls): """ Return the readonly :class:`Path` of the unit circle. For most cases, :func:`Path.circle` will be what you want. """ if cls._unit_circle is None: cls._unit_circle = cls.circle(center=(0, 0), radius=1, readonly=True) return cls._unit_circle @classmethod def circle(cls, center=(0., 0.), radius=1., readonly=False): """ Return a Path representing a circle of a given radius and center. Parameters ---------- center : pair of floats The center of the circle. Default ``(0, 0)``. radius : float The radius of the circle. Default is 1. readonly : bool Whether the created path should have the "readonly" argument set when creating the Path instance. Notes ----- The circle is approximated using cubic Bezier curves. This uses 8 splines around the circle using the approach presented here: Lancaster, Don. `Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_. """ MAGIC = 0.2652031 SQRTHALF = np.sqrt(0.5) MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0) vertices = np.array([ [0.0, -1.0], [MAGIC, -1.0 ], [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], [SQRTHALF, -SQRTHALF], [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC], [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], [SQRTHALF, SQRTHALF], [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], [MAGIC, 1.0], [0.0, 1.0], [-MAGIC, 1.0], [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45], [-SQRTHALF, SQRTHALF], [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45], [-1.0, MAGIC], [-1.0, 0.0], [-1.0, -MAGIC], [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45], [-SQRTHALF, -SQRTHALF], [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45], [-MAGIC, -1.0], [0.0, -1.0], [0.0, -1.0] ], dtype=float) codes = [cls.CURVE4] * 26 codes[0] = cls.MOVETO codes[-1] = cls.CLOSEPOLY return Path(vertices * radius + center, codes, readonly=readonly) _unit_circle_righthalf = None @classmethod def unit_circle_righthalf(cls): """ Return a :class:`Path` of the right half of a unit circle. The circle is approximated using cubic Bezier curves. This uses 4 splines around the circle using the approach presented here: Lancaster, Don. `Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines <http://www.tinaja.com/glib/ellipse4.pdf>`_. """ if cls._unit_circle_righthalf is None: MAGIC = 0.2652031 SQRTHALF = np.sqrt(0.5) MAGIC45 = np.sqrt((MAGIC * MAGIC) / 2.0) vertices = np.array([[0.0, -1.0], [MAGIC, -1.0], [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], [SQRTHALF, -SQRTHALF], [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], [1.0, -MAGIC], [1.0, 0.0], [1.0, MAGIC], [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], [SQRTHALF, SQRTHALF], [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], [MAGIC, 1.0], [0.0, 1.0], [0.0, -1.0]], float) codes = cls.CURVE4 * np.ones(14) codes[0] = cls.MOVETO codes[-1] = cls.CLOSEPOLY cls._unit_circle_righthalf = cls(vertices, codes, readonly=True) return cls._unit_circle_righthalf @classmethod def arc(cls, theta1, theta2, n=None, is_wedge=False): """ Return an arc on the unit circle from angle *theta1* to angle *theta2* (in degrees). If *n* is provided, it is the number of spline segments to make. If *n* is not provided, the number of spline segments is determined based on the delta between *theta1* and *theta2*. Masionobe, L. 2003. `Drawing an elliptical arc using polylines, quadratic or cubic Bezier curves <http://www.spaceroots.org/documents/ellipse/index.html>`_. """ # degrees to radians theta1 *= np.pi / 180.0 theta2 *= np.pi / 180.0 twopi = np.pi * 2.0 halfpi = np.pi * 0.5 eta1 = np.arctan2(np.sin(theta1), np.cos(theta1)) eta2 = np.arctan2(np.sin(theta2), np.cos(theta2)) eta2 -= twopi * np.floor((eta2 - eta1) / twopi) # number of curve segments to make if n is None: n = int(2**np.ceil((eta2 - eta1) / halfpi)) if n < 1: raise ValueError("n must be >= 1 or None") deta = (eta2 - eta1) / n t = np.tan(0.5 * deta) alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0 steps = np.linspace(eta1, eta2, n + 1, True) cos_eta = np.cos(steps) sin_eta = np.sin(steps) xA = cos_eta[:-1] yA = sin_eta[:-1] xA_dot = -yA yA_dot = xA xB = cos_eta[1:] yB = sin_eta[1:] xB_dot = -yB yB_dot = xB if is_wedge: length = n * 3 + 4 vertices = np.zeros((length, 2), float) codes = cls.CURVE4 * np.ones((length, ), cls.code_type) vertices[1] = [xA[0], yA[0]] codes[0:2] = [cls.MOVETO, cls.LINETO] codes[-2:] = [cls.LINETO, cls.CLOSEPOLY] vertex_offset = 2 end = length - 2 else: length = n * 3 + 1 vertices = np.empty((length, 2), float) codes = cls.CURVE4 * np.ones((length, ), cls.code_type) vertices[0] = [xA[0], yA[0]] codes[0] = cls.MOVETO vertex_offset = 1 end = length vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot vertices[vertex_offset + 1:end:3, 0] = xB - alpha * xB_dot vertices[vertex_offset + 1:end:3, 1] = yB - alpha * yB_dot vertices[vertex_offset + 2:end:3, 0] = xB vertices[vertex_offset + 2:end:3, 1] = yB return cls(vertices, codes, readonly=True) @classmethod def wedge(cls, theta1, theta2, n=None): """ Return a wedge of the unit circle from angle *theta1* to angle *theta2* (in degrees). If *n* is provided, it is the number of spline segments to make. If *n* is not provided, the number of spline segments is determined based on the delta between *theta1* and *theta2*. """ return cls.arc(theta1, theta2, n, True) _hatch_dict = maxdict(8) @classmethod def hatch(cls, hatchpattern, density=6): """ Given a hatch specifier, *hatchpattern*, generates a Path that can be used in a repeated hatching pattern. *density* is the number of lines per unit square. """ from matplotlib.hatch import get_path if hatchpattern is None: return None hatch_path = cls._hatch_dict.get((hatchpattern, density)) if hatch_path is not None: return hatch_path hatch_path = get_path(hatchpattern, density) cls._hatch_dict[(hatchpattern, density)] = hatch_path return hatch_path def clip_to_bbox(self, bbox, inside=True): """ Clip the path to the given bounding box. The path must be made up of one or more closed polygons. This algorithm will not behave correctly for unclosed paths. If *inside* is `True`, clip to the inside of the box, otherwise to the outside of the box. """ # Use make_compound_path_from_polys verts = _path.clip_path_to_rect(self, bbox, inside) paths = [Path(poly) for poly in verts] return self.make_compound_path(*paths)
class RendererPS(RendererBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ fontd = maxdict(50) afmfontd = maxdict(50) def __init__(self, width, height, pswriter, imagedpi=72): """ Although postscript itself is dpi independent, we need to imform the image code about a requested dpi to generate high res images and them scale them before embeddin them """ RendererBase.__init__(self) self.width = width self.height = height self._pswriter = pswriter if rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi if rcParams['path.simplify']: self.simplify = (width * imagedpi, height * imagedpi) else: self.simplify = None # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self._hatches = {} self.image_magnification = imagedpi / 72.0 self._clip_paths = {} self._path_collection_id = 0 self.used_characters = {} self.mathtext_parser = MathTextParser("PS") def track_characters(self, font, s): """Keeps track of which characters are required from each font.""" realpath, stat_key = get_realpath_and_stat(font.fname) used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update([ord(x) for x in s]) def merge_used_characters(self, other): for stat_key, (realpath, charset) in other.items(): used_characters = self.used_characters.setdefault( stat_key, (realpath, set())) used_characters[1].update(charset) def set_color(self, r, g, b, store=1): if (r, g, b) != self.color: if r == g and r == b: self._pswriter.write("%1.3f setgray\n" % r) else: self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n" % (r, g, b)) if store: self.color = (r, g, b) def set_linewidth(self, linewidth, store=1): if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n" % linewidth) if store: self.linewidth = linewidth def set_linejoin(self, linejoin, store=1): if linejoin != self.linejoin: self._pswriter.write("%d setlinejoin\n" % linejoin) if store: self.linejoin = linejoin def set_linecap(self, linecap, store=1): if linecap != self.linecap: self._pswriter.write("%d setlinecap\n" % linecap) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=1): if self.linedash is not None: oldo, oldseq = self.linedash if seq_allequal(seq, oldseq): return if seq is not None and len(seq): s = "[%s] %d setdash\n" % (_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset, seq) def set_font(self, fontname, fontsize, store=1): if rcParams['ps.useafm']: return if (fontname, fontsize) != (self.fontname, self.fontsize): out = ("/%s findfont\n" "%1.3f scalefont\n" "setfont\n" % (fontname, fontsize)) self._pswriter.write(out) if store: self.fontname = fontname if store: self.fontsize = fontsize def create_hatch(self, hatch): sidelen = 72 if self._hatches.has_key(hatch): return self._hatches[hatch] name = 'H%d' % len(self._hatches) self._pswriter.write("""\ << /PatternType 1 /PaintType 2 /TilingType 2 /BBox[0 0 %(sidelen)d %(sidelen)d] /XStep %(sidelen)d /YStep %(sidelen)d /PaintProc { pop 0 setlinewidth """ % locals()) self._pswriter.write( self._convert_path(Path.hatch(hatch), Affine2D().scale(72.0))) self._pswriter.write("""\ stroke } bind >> matrix makepattern /%(name)s exch def """ % locals()) self._hatches[hatch] = name return name def get_canvas_width_height(self): 'return the canvas width and height in display coords' return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): """ get the width and height in display coords of the string s with FontPropertry prop """ if rcParams['text.usetex']: texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) return width, height, descent if rcParams['ps.useafm']: if ismath: s = s[1:-1] font = self._get_font_afm(prop) l, b, w, h, d = font.get_str_bbox_and_descent(s) fontsize = prop.get_size_in_points() scale = 0.001 * fontsize w *= scale h *= scale d *= scale return w, h, d font = self._get_font_ttf(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() d /= 64.0 #print s, w, h return w, h, d def flipy(self): 'return true if small y numbers are top for renderer' return False def _get_font_afm(self, prop): key = hash(prop) font = self.afmfontd.get(key) if font is None: fname = findfont(prop, fontext='afm') font = self.afmfontd.get(fname) if font is None: font = AFM(file(findfont(prop, fontext='afm'))) self.afmfontd[fname] = font self.afmfontd[key] = font return font def _get_font_ttf(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) return font def _rgba(self, im): return im.as_rgba_str() def _rgb(self, im): h, w, s = im.as_rgba_str() rgba = npy.fromstring(s, npy.uint8) rgba.shape = (h, w, 4) rgb = rgba[:, :, :3] return h, w, rgb.tostring() def _gray(self, im, rc=0.3, gc=0.59, bc=0.11): rgbat = im.as_rgba_str() rgba = npy.fromstring(rgbat[2], npy.uint8) rgba.shape = (rgbat[0], rgbat[1], 4) rgba_f = rgba.astype(npy.float32) r = rgba_f[:, :, 0] g = rgba_f[:, :, 1] b = rgba_f[:, :, 2] gray = (r * rc + g * gc + b * bc).astype(npy.uint8) return rgbat[0], rgbat[1], gray.tostring() def _hex_lines(self, s, chars_per_line=128): s = binascii.b2a_hex(s) nhex = len(s) lines = [] for i in range(0, nhex, chars_per_line): limit = min(i + chars_per_line, nhex) lines.append(s[i:limit]) return lines def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): """ Draw the Image instance into the current axes; x is the distance in pixels from the left hand side of the canvas and y is the distance from bottom bbox is a matplotlib.transforms.BBox instance for clipping, or None """ im.flipud_out() if im.is_grayscale: h, w, bits = self._gray(im) imagecmd = "image" else: h, w, bits = self._rgb(im) imagecmd = "false 3 colorimage" hexlines = '\n'.join(self._hex_lines(bits)) xscale, yscale = (w / self.image_magnification, h / self.image_magnification) figh = self.height * 72 #print 'values', origin, flipud, figh, h, y clip = [] if bbox is not None: clipx, clipy, clipw, cliph = bbox.bounds clip.append('%s clipbox' % _nums_to_str(clipw, cliph, clipx, clipy)) if clippath is not None: id = self._get_clip_path(clippath, clippath_trans) clip.append('%s' % id) clip = '\n'.join(clip) #y = figh-(y+h) ps = """gsave %(clip)s %(x)s %(y)s translate %(xscale)s %(yscale)s scale /DataString %(w)s string def %(w)s %(h)s 8 [ %(w)s 0 0 -%(h)s 0 %(h)s ] { currentfile DataString readhexstring pop } bind %(imagecmd)s %(hexlines)s grestore """ % locals() self._pswriter.write(ps) # unflip im.flipud_out() def _convert_path(self, path, transform, simplify=None): path = transform.transform_path(path) ps = [] last_points = None for points, code in path.iter_segments(simplify): if code == Path.MOVETO: ps.append("%g %g m" % tuple(points)) elif code == Path.LINETO: ps.append("%g %g l" % tuple(points)) elif code == Path.CURVE3: points = quad2cubic(*(list(last_points[-2:]) + list(points))) ps.append("%g %g %g %g %g %g c" % tuple(points[2:])) elif code == Path.CURVE4: ps.append("%g %g %g %g %g %g c" % tuple(points)) elif code == Path.CLOSEPOLY: ps.append("cl") last_points = points ps = "\n".join(ps) return ps def _get_clip_path(self, clippath, clippath_transform): id = self._clip_paths.get((clippath, clippath_transform)) if id is None: id = 'c%x' % len(self._clip_paths) ps_cmd = ['/%s {' % id] ps_cmd.append(self._convert_path(clippath, clippath_transform)) ps_cmd.extend(['clip', 'newpath', '} bind def\n']) self._pswriter.write('\n'.join(ps_cmd)) self._clip_paths[(clippath, clippath_transform)] = id return id def draw_path(self, gc, path, transform, rgbFace=None): """ Draws a Path instance using the given affine transform. """ ps = self._convert_path(path, transform, self.simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): """ Draw the markers defined by path at each of the positions in x and y. path coordinates are points, x and y coords will be transformed by the transform """ if debugPS: self._pswriter.write('% draw_markers \n') write = self._pswriter.write if rgbFace: if rgbFace[0] == rgbFace[1] and rgbFace[0] == rgbFace[2]: ps_color = '%1.3f setgray' % rgbFace[0] else: ps_color = '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace # construct the generic marker command: ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] # dont want the translate to be global ps_cmd.append(self._convert_path(marker_path, marker_trans)) if rgbFace: ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore']) ps_cmd.extend(['stroke', 'grestore', '} bind def']) tpath = trans.transform_path(path) for vertices, code in tpath.iter_segments(): if len(vertices): x, y = vertices[-2:] ps_cmd.append("%g %g o" % (x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, master_transform, cliprect, clippath, clippath_trans, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): write = self._pswriter.write path_codes = [] for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): name = 'p%x_%x' % (self._path_collection_id, i) ps_cmd = ['/%s {' % name, 'newpath', 'translate'] ps_cmd.append(self._convert_path(path, transform)) ps_cmd.extend(['} bind def\n']) write('\n'.join(ps_cmd)) path_codes.append(name) for xo, yo, path_id, gc, rgbFace in self._iter_collection( path_codes, cliprect, clippath, clippath_trans, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!'): """ draw a Text instance """ w, h, bl = self.get_text_width_height_descent(s, prop, ismath) fontsize = prop.get_size_in_points() corr = 0 #w/2*(fontsize-10)/10 pos = _nums_to_str(x - corr, y) thetext = 'psmarker%d' % self.textcnt color = '%1.3f,%1.3f,%1.3f' % gc.get_rgb()[:3] fontcmd = { 'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}' }.get(rcParams['font.family'], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) self.psfrag.append(r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % (thetext, angle, fontsize, fontsize * 1.25, tex)) ps = """\ gsave %(pos)s moveto (%(thetext)s) show grestore """ % locals() self._pswriter.write(ps) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath): """ draw a Text instance """ # local to avoid repeated attribute lookups write = self._pswriter.write if debugPS: write("% text\n") if ismath == 'TeX': return self.tex(gc, x, y, s, prop, angle) elif ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) elif isinstance(s, unicode): return self.draw_unicode(gc, x, y, s, prop, angle) elif rcParams['ps.useafm']: font = self._get_font_afm(prop) l, b, w, h = font.get_str_bbox(s) fontsize = prop.get_size_in_points() l *= 0.001 * fontsize b *= 0.001 * fontsize w *= 0.001 * fontsize h *= 0.001 * fontsize if angle == 90: l, b = -b, l # todo generalize for arb rotations pos = _nums_to_str(x - l, y - b) thetext = '(%s)' % s fontname = font.get_fontname() fontsize = prop.get_size_in_points() rotate = '%1.1f rotate' % angle setcolor = '%1.3f %1.3f %1.3f setrgbcolor' % gc.get_rgb()[:3] #h = 0 ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(pos)s moveto %(rotate)s %(thetext)s %(setcolor)s show grestore """ % locals() self._draw_ps(ps, gc, None) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1, 0, 0, 6)], prop.get_size_in_points()) write("%s m\n" % _nums_to_str(x, y)) if angle: write("gsave\n") write("%s rotate\n" % _num_to_str(angle)) descent = font.get_descent() / 64.0 if descent: write("0 %s rmoveto\n" % _num_to_str(descent)) write("(%s) show\n" % quote_ps_string(s)) if angle: write("grestore\n") def new_gc(self): return GraphicsContextPS() def draw_unicode(self, gc, x, y, s, prop, angle): """draw a unicode string. ps doesn't have unicode support, so we have to do this the hard way """ if rcParams['ps.useafm']: self.set_color(*gc.get_rgb()) font = self._get_font_afm(prop) fontname = font.get_fontname() fontsize = prop.get_size_in_points() scale = 0.001 * fontsize thisx = 0 thisy = font.get_str_bbox_and_descent(s)[4] * scale last_name = None lines = [] for c in s: name = uni2type1.get(ord(c), 'question') try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') if last_name is not None: kern = font.get_kern_dist_from_name(last_name, name) else: kern = 0 last_name = name thisx += kern * scale lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name)) thisx += width * scale thetext = "\n".join(lines) ps = """\ gsave /%(fontname)s findfont %(fontsize)s scalefont setfont %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self.track_characters(font, s) self.set_color(*gc.get_rgb()) self.set_font(font.get_sfnt()[(1, 0, 0, 6)], prop.get_size_in_points()) cmap = font.get_charmap() lastgind = None #print 'text', s lines = [] thisx = 0 thisy = font.get_descent() / 64.0 for c in s: ccode = ord(c) gind = cmap.get(ccode) if gind is None: ccode = ord('?') name = '.notdef' gind = 0 else: name = font.get_glyph_name(gind) glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) if lastgind is not None: kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) else: kern = 0 lastgind = gind thisx += kern / 64.0 lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name)) thisx += glyph.linearHoriAdvance / 65536.0 thetext = '\n'.join(lines) ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def draw_mathtext(self, gc, x, y, s, prop, angle): """ Draw the math text using matplotlib.mathtext """ if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, pswriter, used_characters = \ self.mathtext_parser.parse(s, 72, prop) self.merge_used_characters(used_characters) self.set_color(*gc.get_rgb()) thetext = pswriter.getvalue() ps = """gsave %(x)f %(y)f translate %(angle)f rotate %(thetext)s grestore """ % locals() self._pswriter.write(ps) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript sniplet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% " + command + "\n") mightstroke = (gc.get_linewidth() > 0.0 and (len(gc.get_rgb()) <= 3 or gc.get_rgb()[3] != 0.0)) stroke = stroke and mightstroke fill = (fill and rgbFace is not None and (len(rgbFace) <= 3 or rgbFace[3] != 0.0)) if mightstroke: self.set_linewidth(gc.get_linewidth()) jint = gc.get_joinstyle() self.set_linejoin(jint) cint = gc.get_capstyle() self.set_linecap(cint) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') cliprect = gc.get_clip_rectangle() if cliprect: x, y, w, h = cliprect.bounds write('%1.4g %1.4g %1.4g %1.4g clipbox\n' % (w, h, x, y)) clippath, clippath_trans = gc.get_clip_path() if clippath: id = self._get_clip_path(clippath, clippath_trans) write('%s\n' % id) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke: write("gsave\n") self.set_color(store=0, *rgbFace[:3]) write("fill\n") if stroke: write("grestore\n") hatch = gc.get_hatch() if hatch: hatch_name = self.create_hatch(hatch) write("gsave\n") write("[/Pattern [/DeviceRGB]] setcolorspace %f %f %f " % gc.get_rgb()[:3]) write("%s setcolor fill grestore\n" % hatch_name) if stroke: write("stroke\n") write("grestore\n")
class RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forward the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() self.gc.set_dpi(self.dpi) self.mathtext_parser = MathTextParser('MacOSX') def set_width_height (self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) linewidth = gc.get_linewidth() gc.draw_path(path, transform, linewidth, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) linewidth = gc.get_linewidth() gc.draw_markers(marker_path, marker_trans, path, trans, linewidth, rgbFace) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): if offset_position=='data': offset_position = True else: offset_position = False path_ids = [] for path, transform in self._iter_collection_raw_paths( master_transform, paths, all_transforms): path_ids.append((path, transform)) master_transform = master_transform.get_matrix() all_transforms = [t.get_matrix() for t in all_transforms] offsetTrans = offsetTrans.get_matrix() gc.draw_path_collection(master_transform, path_ids, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, offset_position) def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, edgecolors): gc.draw_quad_mesh(master_transform.get_matrix(), meshWidth, meshHeight, coordinates, offsets, offsetTrans.get_matrix(), facecolors, antialiased, edgecolors) def new_gc(self): self.gc.save() self.gc.set_hatch(None) self.gc._alpha = 1.0 self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA return self.gc def draw_gouraud_triangle(self, gc, points, colors, transform): points = transform.transform(points) gc.draw_gouraud_triangle(points, colors) def draw_image(self, gc, x, y, im): im.flipud_out() nrows, ncols, data = im.as_rgba_str() gc.draw_image(x, y, nrows, ncols, data) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get(key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): ox, oy, width, height, descent, image, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) gc.draw_mathtext(x, y, angle, 255 - image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath=='TeX': # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: ox, oy, width, height, descent, fonts, used_characters = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) width, height, descent = self.gc.get_text_width_height_descent(unicode(s), family, size, weight, style) return width, height, 0.0*descent def flipy(self): return False def points_to_pixels(self, points): return points/72.0 * self.dpi def option_image_nocomposite(self): return True
class RendererMac(RendererBase): """ The renderer handles drawing/rendering operations. Most of the renderer's methods forwards the command to the renderer's graphics context. The renderer does not wrap a C object and is written in pure Python. """ texd = maxdict(50) # a cache of tex image rasters def __init__(self, dpi, width, height): RendererBase.__init__(self) self.dpi = dpi self.width = width self.height = height self.gc = GraphicsContextMac() def set_width_height(self, width, height): self.width, self.height = width, height def draw_path(self, gc, path, transform, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) gc.draw_path(path, transform, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): if rgbFace is not None: rgbFace = tuple(rgbFace) gc.draw_markers(marker_path, marker_trans, path, trans, rgbFace) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls): cliprect = gc.get_clip_rectangle() clippath, clippath_transform = gc.get_clip_path() gc.draw_path_collection(master_transform, cliprect, clippath, clippath_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds) def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, showedges): cliprect = gc.get_clip_rectangle() clippath, clippath_transform = gc.get_clip_path() gc.draw_quad_mesh(master_transform, cliprect, clippath, clippath_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, showedges) def new_gc(self): self.gc.save() self.gc.set_hatch(None) return self.gc def draw_image(self, gc, x, y, im): im.flipud_out() nrows, ncols, data = im.as_rgba_str() gc.draw_image(x, y, nrows, ncols, data, gc.get_clip_rectangle(), *gc.get_clip_path()) im.flipud_out() def draw_tex(self, gc, x, y, s, prop, angle): # todo, handle props, angle, origins size = prop.get_size_in_points() texmanager = self.get_texmanager() key = s, size, self.dpi, angle, texmanager.get_font_config() im = self.texd.get( key) # Not sure what this does; just copied from backend_agg.py if im is None: Z = texmanager.get_grey(s, size, self.dpi) Z = numpy.array(255.0 - Z * 255.0, numpy.uint8) gc.draw_mathtext(x, y, angle, Z) def _draw_mathtext(self, gc, x, y, s, prop, angle): if not HAVE_MATHTEX: return m = Mathtex(s, rcParams['mathtext.fontset'], prop.get_size_in_points(), self.dpi, rcParams['mathtext.default'], cache=True) b = MathtexBackendImage() m.render_to_backend(b) gc.draw_mathtext(x, y, angle, 255 - b.image.as_array()) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) else: family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) gc.draw_text(x, y, unicode(s), family, size, weight, style, angle) def get_text_width_height_descent(self, s, prop, ismath): if ismath == 'TeX': # todo: handle props texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() w, h, d = texmanager.get_text_width_height_descent(s, fontsize, renderer=self) return w, h, d if ismath: if HAVE_MATHTEX: m = Mathtex(s, rcParams['mathtext.fontset'], prop.get_size_in_points(), self.dpi, rcParams['mathtext.default'], cache=True) return m.width, m.height, m.depth else: warnings.warn( 'matplotlib was compiled without mathtex support. ' + 'Math will not be rendered.') return 0.0, 0.0, 0.0 family = prop.get_family() weight = prop.get_weight() style = prop.get_style() points = prop.get_size_in_points() size = self.points_to_pixels(points) width, height, descent = self.gc.get_text_width_height_descent( unicode(s), family, size, weight, style) return width, height, 0.0 * descent def flipy(self): return False def points_to_pixels(self, points): return points / 72.0 * self.dpi def option_image_nocomposite(self): return True