class RendererHTMLCanvas(RendererBase): def __init__(self, ctx, width, height, dpi, fig): super().__init__() self.fig = fig self.ctx = ctx self.width = width self.height = height self.ctx.width = self.width self.ctx.height = self.height self.dpi = dpi self.fontd = maxdict(50) self.mathtext_parser = MathTextParser("bitmap") def new_gc(self): return GraphicsContextHTMLCanvas(renderer=self) def points_to_pixels(self, points): return (points / 72.0) * self.dpi def _matplotlib_color_to_CSS(self, color, alpha, alpha_overrides, is_RGB=True): if not is_RGB: R, G, B, alpha = colorConverter.to_rgba(color) color = (R, G, B) if (len(color) == 4) and (alpha is None): alpha = color[3] if alpha is None: CSS_color = rgb2hex(color[:3]) else: R = int(color[0] * 255) G = int(color[1] * 255) B = int(color[2] * 255) if len(color) == 3 or alpha_overrides: CSS_color = f"""rgba({R:d}, {G:d}, {B:d}, {alpha:.3g})""" else: CSS_color = """rgba({:d}, {:d}, {:d}, {:.3g})""".format( R, G, B, color[3]) return CSS_color def _set_style(self, gc, rgbFace=None): if rgbFace is not None: self.ctx.fillStyle = self._matplotlib_color_to_CSS( rgbFace, gc.get_alpha(), gc.get_forced_alpha()) if gc.get_capstyle(): self.ctx.lineCap = _capstyle_d[gc.get_capstyle()] self.ctx.strokeStyle = self._matplotlib_color_to_CSS( gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha()) self.ctx.lineWidth = self.points_to_pixels(gc.get_linewidth()) def _path_helper(self, ctx, path, transform, clip=None): ctx.beginPath() for points, code in path.iter_segments(transform, remove_nans=True, clip=clip): if code == Path.MOVETO: ctx.moveTo(points[0], points[1]) elif code == Path.LINETO: ctx.lineTo(points[0], points[1]) elif code == Path.CURVE3: ctx.quadraticCurveTo(*points) elif code == Path.CURVE4: ctx.bezierCurveTo(*points) elif code == Path.CLOSEPOLY: ctx.closePath() def draw_path(self, gc, path, transform, rgbFace=None): self._set_style(gc, rgbFace) if rgbFace is None and gc.get_hatch() is None: figure_clip = (0, 0, self.width, self.height) else: figure_clip = None transform += Affine2D().scale(1, -1).translate(0, self.height) self._path_helper(self.ctx, path, transform, figure_clip) if rgbFace is not None: self.ctx.fill() self.ctx.fillStyle = "#000000" if gc.stroke: self.ctx.stroke() def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): super().draw_markers(gc, marker_path, marker_trans, path, trans, rgbFace) def draw_image(self, gc, x, y, im, transform=None): im = np.flipud(im) h, w, d = im.shape y = self.ctx.height - y - h im = np.ravel(np.uint8(np.reshape(im, (h * w * d, -1)))).tobytes() pixels_proxy = create_proxy(im) pixels_buf = pixels_proxy.getBuffer("u8clamped") img_data = ImageData.new(pixels_buf.data, w, h) self.ctx.save() in_memory_canvas = document.createElement("canvas") in_memory_canvas.width = w in_memory_canvas.height = h in_memory_canvas_context = in_memory_canvas.getContext("2d") in_memory_canvas_context.putImageData(img_data, 0, 0) self.ctx.drawImage(in_memory_canvas, x, y, w, h) self.ctx.restore() pixels_proxy.destroy() pixels_buf.release() def _get_font(self, prop): key = hash(prop) font_value = self.fontd.get(key) if font_value is None: fname = findfont(prop) font_value = self.fontd.get(fname) if font_value is None: font = FT2Font(str(fname)) font_file_name = fname[fname.rfind("/") + 1:] font_value = font, font_file_name self.fontd[fname] = font_value self.fontd[key] = font_value font, font_file_name = font_value font.clear() font.set_size(prop.get_size_in_points(), self.dpi) return font, font_file_name def get_text_width_height_descent(self, s, prop, ismath): if ismath: image, d = self.mathtext_parser.parse(s, self.dpi, prop) w, h = image.get_width(), image.get_height() else: font, _ = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 h /= 64.0 d = font.get_descent() / 64.0 return w, h, d def _draw_math_text(self, gc, x, y, s, prop, angle): rgba, descent = self.mathtext_parser.to_rgba(s, gc.get_rgb(), self.dpi, prop.get_size_in_points()) height, width, _ = rgba.shape angle = math.radians(angle) if angle != 0: self.ctx.save() self.ctx.translate(x, y) self.ctx.rotate(-angle) self.ctx.translate(-x, -y) self.draw_image(gc, x, -y - descent, np.flipud(rgba)) if angle != 0: self.ctx.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def _load_font_into_web(loaded_face): document.fonts.add(loaded_face) window.font_counter += 1 self.fig.draw_idle() if ismath: self._draw_math_text(gc, x, y, s, prop, angle) return angle = math.radians(angle) width, height, descent = self.get_text_width_height_descent( s, prop, ismath) x -= math.sin(angle) * descent y -= math.cos(angle) * descent - self.ctx.height font_size = self.points_to_pixels(prop.get_size_in_points()) _, font_file_name = self._get_font(prop) font_face_arguments = ( prop.get_name(), f"url({_base_fonts_url + font_file_name})", ) # The following snippet loads a font into the browser's # environment if it wasn't loaded before. This check is necessary # to help us avoid loading the same font multiple times. Further, # it helps us to avoid the infinite loop of # load font --> redraw --> load font --> redraw --> .... if font_face_arguments not in _font_set: _font_set.add(font_face_arguments) f = FontFace.new(*font_face_arguments) f.load().then(_load_font_into_web) font_property_string = "{} {} {:.3g}px {}, {}".format( prop.get_style(), prop.get_weight(), font_size, prop.get_name(), prop.get_family()[0], ) if angle != 0: self.ctx.save() self.ctx.translate(x, y) self.ctx.rotate(-angle) self.ctx.translate(-x, -y) self.ctx.font = font_property_string self.ctx.fillStyle = self._matplotlib_color_to_CSS( gc.get_rgb(), gc.get_alpha(), gc.get_forced_alpha()) self.ctx.fillText(s, x, y) self.ctx.fillStyle = "#000000" if angle != 0: self.ctx.restore()
class RendererH5Canvas(RendererBase): """The renderer handles drawing/rendering operations.""" fontd = maxdict(50) def __init__(self, width, height, ctx, dpi=72): self.width = width self.height = height self.dpi = dpi self.ctx = ctx self._image_count = 0 # used to uniquely label each image created in this figure... # define the js context self.ctx.width = width self.ctx.height = height #self.ctx.textAlign = "center"; self.ctx.textBaseline = "alphabetic" self.flip = Affine2D().scale(1, -1).translate(0, height) self.mathtext_parser = MathTextParser('bitmap') self._path_time = 0 self._text_time = 0 self._marker_time = 0 self._sub_time = 0 self._last_clip = None self._last_clip_path = None self._clip_count = 0 def _set_style(self, gc, rgbFace=None): ctx = self.ctx if rgbFace is not None: ctx.fillStyle = mpl_to_css_color(rgbFace, gc.get_alpha()) ctx.strokeStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) if gc.get_capstyle(): ctx.lineCap = _capstyle_d[gc.get_capstyle()] ctx.lineWidth = self.points_to_pixels(gc.get_linewidth()) def _path_to_h5(self, ctx, path, transform, clip=None, stroke=True, dashes=(None, None)): """Iterate over a path and produce h5 drawing directives.""" transform = transform + self.flip ctx.beginPath() current_point = None dash_offset, dash_pattern = dashes if dash_pattern is not None: dash_offset = self.points_to_pixels(dash_offset) dash_pattern = tuple([self.points_to_pixels(dash) for dash in dash_pattern]) for points, code in path.iter_segments(transform, clip=clip): # Shift all points by half a pixel, so that integer coordinates are aligned with pixel centers instead of edges # This prevents lines that are one pixel wide and aligned with the pixel grid from being rendered as a two-pixel wide line # This happens because HTML Canvas defines (0, 0) as the *top left* of a pixel instead of the center, # which causes all integer-valued coordinates to fall exactly between pixels points += 0.5 if code == Path.MOVETO: ctx.moveTo(points[0], points[1]) current_point = (points[0], points[1]) elif code == Path.LINETO: t = time.time() if (dash_pattern is None) or (current_point is None): ctx.lineTo(points[0], points[1]) else: dash_offset = ctx.dashedLine(current_point[0], current_point[1], points[0], points[1], (dash_offset, dash_pattern)) self._sub_time += time.time() - t current_point = (points[0], points[1]) elif code == Path.CURVE3: ctx.quadraticCurveTo(*points) current_point = (points[2], points[3]) elif code == Path.CURVE4: ctx.bezierCurveTo(*points) current_point = (points[4], points[5]) else: pass if stroke: ctx.stroke() def _do_path_clip(self, ctx, clip): self._clip_count += 1 ctx.save() ctx.beginPath() ctx.moveTo(clip[0],clip[1]) ctx.lineTo(clip[2],clip[1]) ctx.lineTo(clip[2],clip[3]) ctx.lineTo(clip[0],clip[3]) ctx.clip() def draw_path(self, gc, path, transform, rgbFace=None): t = time.time() self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) clippath, cliptrans = gc.get_clip_path() ctx = self.ctx if clippath is not None and self._last_clip_path != clippath: ctx.restore() ctx.save() self._path_to_h5(ctx, clippath, cliptrans, None, stroke=False) ctx.clip() self._last_clip_path = clippath if self._last_clip != clip and clip is not None and clippath is None: ctx.restore() self._do_path_clip(ctx, clip) self._last_clip = clip if clip is None and clippath is None and (self._last_clip is not None or self._last_clip_path is not None): self._reset_clip() if rgbFace is None and gc.get_hatch() is None: figure_clip = (0, 0, self.width, self.height) else: figure_clip = None self._path_to_h5(ctx, path, transform, figure_clip, dashes=gc.get_dashes()) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._path_time += time.time() - t def _get_gc_clip_svg(self, gc): cliprect = gc.get_clip_rectangle() if cliprect is not None: x, y, w, h = cliprect.bounds y = self.height-(y+h) return (x,y,x+w,y+h) return None def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): t = time.time() for vertices, codes in path.iter_segments(trans, simplify=False): if len(vertices): x,y = vertices[-2:] self._set_style(gc, rgbFace) clip = self._get_gc_clip_svg(gc) ctx = self.ctx self._path_to_h5(ctx, marker_path, marker_trans + Affine2D().translate(x, y), clip) if rgbFace is not None: ctx.fill() ctx.fillStyle = '#000000' self._marker_time += time.time() - t def _slipstream_png(self, x, y, im_buffer, width, height): """Insert image directly into HTML canvas as base64-encoded PNG.""" # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) # Write the image into a WebPNG object f = WebPNG() _png.write_png(im_buffer, width, height, f) # Write test PNG as file as well #_png.write_png(im_buffer, width, height, 'canvas_image_%d.png' % (self._image_count,)) # Extract the base64-encoded PNG and send it to the canvas uname = str(uuid.uuid1()).replace("-","") #self.ctx._context_name + str(self._image_count) # try to use a unique image name enc = "var canvas_image_%s = 'data:image/png;base64,%s';" % (uname, f.get_b64()) s = "function imageLoaded_%s(ev) {\nim = ev.target;\nim_left_to_load_%s -=1;\nif (im_left_to_load_%s == 0) frame_body_%s();\n}\ncanv_im_%s = new Image();\ncanv_im_%s.onload = imageLoaded_%s;\ncanv_im_%s.src = canvas_image_%s;\n" % \ (uname, self.ctx._context_name, self.ctx._context_name, self.ctx._context_name, uname, uname, uname, uname, uname) self.ctx.add_header(enc) self.ctx.add_header(s) # Once the base64 encoded image has been received, draw it into the canvas self.ctx.write("%s.drawImage(canv_im_%s, %g, %g, %g, %g);" % (self.ctx._context_name, uname, x, y, width, height)) # draw the image as loaded into canv_im_%d... self._image_count += 1 def _reset_clip(self): self.ctx.restore() self._last_clip = None self._last_clip_path = None #<1.0.0: def draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None): #1.0.0 and up: def draw_image(self, gc, x, y, im, clippath=None): #API for draw image changed between 0.99 and 1.0.0 def draw_image(self, *args, **kwargs): x, y, im = args[:3] try: h,w = im.get_size_out() except AttributeError: x, y, im = args[1:4] h,w = im.get_size_out() clippath = (kwargs.has_key('clippath') and kwargs['clippath'] or None) if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() if clippath is not None: self._path_to_h5(self.ctx,clippath, clippath_trans, stroke=False) self.ctx.save() self.ctx.clip() (x,y) = self.flip.transform((x,y)) im.flipud_out() rows, cols, im_buffer = im.as_rgba_str() self._slipstream_png(x, (y-h), im_buffer, cols, rows) if clippath is not None: self.ctx.restore() def _get_font(self, prop): key = hash(prop) font = self.fontd.get(key) if font is None: fname = findfont(prop) font = self.fontd.get(fname) if font is None: font = FT2Font(str(fname)) self.fontd[fname] = font self.fontd[key] = font font.clear() font.set_size(prop.get_size_in_points(), self.dpi) return font def draw_tex(self, gc, x, y, s, prop, angle, ismath=False): logger.error("Tex support is currently not implemented. Text element '%s' will not be displayed..." % s) def draw_text(self, gc, x, y, s, prop, angle, ismath=False): if self._last_clip is not None or self._last_clip_path is not None: self._reset_clip() t = time.time() if ismath: self._draw_mathtext(gc, x, y, s, prop, angle) return angle = math.radians(angle) width, height, descent = self.get_text_width_height_descent(s, prop, ismath) x -= math.sin(angle) * descent y -= math.cos(angle) * descent ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) font_size = self.points_to_pixels(prop.get_size_in_points()) font_str = '%s %s %.3gpx %s, %s' % (prop.get_style(), prop.get_weight(), font_size, prop.get_name(), prop.get_family()[0]) ctx.font = font_str # Set the text color, draw the text and reset the color to black afterwards ctx.fillStyle = mpl_to_css_color(gc.get_rgb(), gc.get_alpha()) ctx.fillText(unicode(s), x, y) ctx.fillStyle = '#000000' if angle != 0: ctx.restore() self._text_time = time.time() - t def _draw_mathtext(self, gc, x, y, s, prop, angle): """Draw math text using matplotlib.mathtext.""" # Render math string as an image at the configured DPI, and get the image dimensions and baseline depth rgba, descent = self.mathtext_parser.to_rgba(s, color=gc.get_rgb(), dpi=self.dpi, fontsize=prop.get_size_in_points()) height, width, tmp = rgba.shape angle = math.radians(angle) # Shift x, y (top left corner) to the nearest CSS pixel edge, to prevent resampling and consequent image blurring x = math.floor(x + 0.5) y = math.floor(y + 1.5) ctx = self.ctx if angle != 0: ctx.save() ctx.translate(x, y) ctx.rotate(-angle) ctx.translate(-x, -y) # Insert math text image into stream, and adjust x, y reference point to be at top left of image self._slipstream_png(x, y - height, rgba.tostring(), width, height) if angle != 0: ctx.restore() def flipy(self): return True def get_canvas_width_height(self): return self.width, self.height def get_text_width_height_descent(self, s, prop, ismath): if ismath: image, d = self.mathtext_parser.parse(s, self.dpi, prop) w, h = image.get_width(), image.get_height() else: font = self._get_font(prop) font.set_text(s, 0.0, flags=LOAD_NO_HINTING) w, h = font.get_width_height() w /= 64.0 # convert from subpixels h /= 64.0 d = font.get_descent() / 64.0 return w, h, d def new_gc(self): return GraphicsContextH5Canvas() def points_to_pixels(self, points): # The standard desktop-publishing (Postscript) point is 1/72 of an inch return points/72.0 * self.dpi
class 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