12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241 |
- from collections import OrderedDict
- import base64
- import gzip
- import hashlib
- import io
- import itertools
- import logging
- import re
- import uuid
- import numpy as np
- from matplotlib import cbook, __version__, rcParams
- from matplotlib.backend_bases import (
- _Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
- from matplotlib.backends.backend_mixed import MixedModeRenderer
- from matplotlib.colors import rgb2hex
- from matplotlib.font_manager import findfont, get_font
- from matplotlib.ft2font import LOAD_NO_HINTING
- from matplotlib.mathtext import MathTextParser
- from matplotlib.path import Path
- from matplotlib import _path
- from matplotlib.transforms import Affine2D, Affine2DBase
- from matplotlib import _png
- _log = logging.getLogger(__name__)
- backend_version = __version__
- # ----------------------------------------------------------------------
- # SimpleXMLWriter class
- #
- # Based on an original by Fredrik Lundh, but modified here to:
- # 1. Support modern Python idioms
- # 2. Remove encoding support (it's handled by the file writer instead)
- # 3. Support proper indentation
- # 4. Minify things a little bit
- # --------------------------------------------------------------------
- # The SimpleXMLWriter module is
- #
- # Copyright (c) 2001-2004 by Fredrik Lundh
- #
- # By obtaining, using, and/or copying this software and/or its
- # associated documentation, you agree that you have read, understood,
- # and will comply with the following terms and conditions:
- #
- # Permission to use, copy, modify, and distribute this software and
- # its associated documentation for any purpose and without fee is
- # hereby granted, provided that the above copyright notice appears in
- # all copies, and that both that copyright notice and this permission
- # notice appear in supporting documentation, and that the name of
- # Secret Labs AB or the author not be used in advertising or publicity
- # pertaining to distribution of the software without specific, written
- # prior permission.
- #
- # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
- # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
- # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
- # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
- # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
- # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
- # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
- # OF THIS SOFTWARE.
- # --------------------------------------------------------------------
- def escape_cdata(s):
- s = s.replace("&", "&")
- s = s.replace("<", "<")
- s = s.replace(">", ">")
- return s
- _escape_xml_comment = re.compile(r'-(?=-)')
- def escape_comment(s):
- s = escape_cdata(s)
- return _escape_xml_comment.sub('- ', s)
- def escape_attrib(s):
- s = s.replace("&", "&")
- s = s.replace("'", "'")
- s = s.replace('"', """)
- s = s.replace("<", "<")
- s = s.replace(">", ">")
- return s
- def short_float_fmt(x):
- """
- Create a short string representation of a float, which is %f
- formatting with trailing zeros and the decimal point removed.
- """
- return '{0:f}'.format(x).rstrip('0').rstrip('.')
- class XMLWriter:
- """
- Parameters
- ----------
- file : writable text file-like object
- """
- def __init__(self, file):
- self.__write = file.write
- if hasattr(file, "flush"):
- self.flush = file.flush
- self.__open = 0 # true if start tag is open
- self.__tags = []
- self.__data = []
- self.__indentation = " " * 64
- def __flush(self, indent=True):
- # flush internal buffers
- if self.__open:
- if indent:
- self.__write(">\n")
- else:
- self.__write(">")
- self.__open = 0
- if self.__data:
- data = ''.join(self.__data)
- self.__write(escape_cdata(data))
- self.__data = []
- def start(self, tag, attrib={}, **extra):
- """
- Opens a new element. Attributes can be given as keyword
- arguments, or as a string/string dictionary. The method returns
- an opaque identifier that can be passed to the :meth:`close`
- method, to close all open elements up to and including this one.
- Parameters
- ----------
- tag
- Element tag.
- attrib
- Attribute dictionary. Alternatively, attributes can be given as
- keyword arguments.
- Returns
- -------
- An element identifier.
- """
- self.__flush()
- tag = escape_cdata(tag)
- self.__data = []
- self.__tags.append(tag)
- self.__write(self.__indentation[:len(self.__tags) - 1])
- self.__write("<%s" % tag)
- for k, v in sorted({**attrib, **extra}.items()):
- if v:
- k = escape_cdata(k)
- v = escape_attrib(v)
- self.__write(' %s="%s"' % (k, v))
- self.__open = 1
- return len(self.__tags) - 1
- def comment(self, comment):
- """
- Adds a comment to the output stream.
- Parameters
- ----------
- comment : str
- Comment text.
- """
- self.__flush()
- self.__write(self.__indentation[:len(self.__tags)])
- self.__write("<!-- %s -->\n" % escape_comment(comment))
- def data(self, text):
- """
- Adds character data to the output stream.
- Parameters
- ----------
- text : str
- Character data.
- """
- self.__data.append(text)
- def end(self, tag=None, indent=True):
- """
- Closes the current element (opened by the most recent call to
- :meth:`start`).
- Parameters
- ----------
- tag
- Element tag. If given, the tag must match the start tag. If
- omitted, the current element is closed.
- """
- if tag:
- assert self.__tags, "unbalanced end(%s)" % tag
- assert escape_cdata(tag) == self.__tags[-1], \
- "expected end(%s), got %s" % (self.__tags[-1], tag)
- else:
- assert self.__tags, "unbalanced end()"
- tag = self.__tags.pop()
- if self.__data:
- self.__flush(indent)
- elif self.__open:
- self.__open = 0
- self.__write("/>\n")
- return
- if indent:
- self.__write(self.__indentation[:len(self.__tags)])
- self.__write("</%s>\n" % tag)
- def close(self, id):
- """
- Closes open elements, up to (and including) the element identified
- by the given identifier.
- Parameters
- ----------
- id
- Element identifier, as returned by the :meth:`start` method.
- """
- while len(self.__tags) > id:
- self.end()
- def element(self, tag, text=None, attrib={}, **extra):
- """
- Adds an entire element. This is the same as calling :meth:`start`,
- :meth:`data`, and :meth:`end` in sequence. The *text* argument can be
- omitted.
- """
- self.start(tag, attrib, **extra)
- if text:
- self.data(text)
- self.end(indent=False)
- def flush(self):
- """Flushes the output stream."""
- pass # replaced by the constructor
- def generate_transform(transform_list=[]):
- if len(transform_list):
- output = io.StringIO()
- for type, value in transform_list:
- if (type == 'scale' and (value == (1,) or value == (1, 1))
- or type == 'translate' and value == (0, 0)
- or type == 'rotate' and value == (0,)):
- continue
- if type == 'matrix' and isinstance(value, Affine2DBase):
- value = value.to_values()
- output.write('%s(%s)' % (
- type, ' '.join(short_float_fmt(x) for x in value)))
- return output.getvalue()
- return ''
- def generate_css(attrib={}):
- if attrib:
- output = io.StringIO()
- attrib = sorted(attrib.items())
- for k, v in attrib:
- k = escape_attrib(k)
- v = escape_attrib(v)
- output.write("%s:%s;" % (k, v))
- return output.getvalue()
- return ''
- _capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'}
- class RendererSVG(RendererBase):
- def __init__(self, width, height, svgwriter, basename=None, image_dpi=72):
- self.width = width
- self.height = height
- self.writer = XMLWriter(svgwriter)
- self.image_dpi = image_dpi # actual dpi at which we rasterize stuff
- self._groupd = {}
- self.basename = basename
- self._image_counter = itertools.count()
- self._clipd = OrderedDict()
- self._markers = {}
- self._path_collection_id = 0
- self._hatchd = OrderedDict()
- self._has_gouraud = False
- self._n_gradients = 0
- self._fonts = OrderedDict()
- self.mathtext_parser = MathTextParser('SVG')
- RendererBase.__init__(self)
- self._glyph_map = dict()
- str_height = short_float_fmt(height)
- str_width = short_float_fmt(width)
- svgwriter.write(svgProlog)
- self._start_id = self.writer.start(
- 'svg',
- width='%spt' % str_width,
- height='%spt' % str_height,
- viewBox='0 0 %s %s' % (str_width, str_height),
- xmlns="http://www.w3.org/2000/svg",
- version="1.1",
- attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"})
- self._write_default_style()
- def finalize(self):
- self._write_clips()
- self._write_hatches()
- self.writer.close(self._start_id)
- self.writer.flush()
- def _write_default_style(self):
- writer = self.writer
- default_style = generate_css({
- 'stroke-linejoin': 'round',
- 'stroke-linecap': 'butt'})
- writer.start('defs')
- writer.start('style', type='text/css')
- writer.data('*{%s}\n' % default_style)
- writer.end('style')
- writer.end('defs')
- def _make_id(self, type, content):
- salt = rcParams['svg.hashsalt']
- if salt is None:
- salt = str(uuid.uuid4())
- m = hashlib.md5()
- m.update(salt.encode('utf8'))
- m.update(str(content).encode('utf8'))
- return '%s%s' % (type, m.hexdigest()[:10])
- def _make_flip_transform(self, transform):
- return (transform +
- Affine2D()
- .scale(1.0, -1.0)
- .translate(0.0, self.height))
- def _get_font(self, prop):
- fname = findfont(prop)
- font = get_font(fname)
- font.clear()
- size = prop.get_size_in_points()
- font.set_size(size, 72.0)
- return font
- def _get_hatch(self, gc, rgbFace):
- """
- Create a new hatch pattern
- """
- if rgbFace is not None:
- rgbFace = tuple(rgbFace)
- edge = gc.get_hatch_color()
- if edge is not None:
- edge = tuple(edge)
- dictkey = (gc.get_hatch(), rgbFace, edge)
- oid = self._hatchd.get(dictkey)
- if oid is None:
- oid = self._make_id('h', dictkey)
- self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid)
- else:
- _, oid = oid
- return oid
- def _write_hatches(self):
- if not len(self._hatchd):
- return
- HATCH_SIZE = 72
- writer = self.writer
- writer.start('defs')
- for (path, face, stroke), oid in self._hatchd.values():
- writer.start(
- 'pattern',
- id=oid,
- patternUnits="userSpaceOnUse",
- x="0", y="0", width=str(HATCH_SIZE),
- height=str(HATCH_SIZE))
- path_data = self._convert_path(
- path,
- Affine2D()
- .scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE),
- simplify=False)
- if face is None:
- fill = 'none'
- else:
- fill = rgb2hex(face)
- writer.element(
- 'rect',
- x="0", y="0", width=str(HATCH_SIZE+1),
- height=str(HATCH_SIZE+1),
- fill=fill)
- writer.element(
- 'path',
- d=path_data,
- style=generate_css({
- 'fill': rgb2hex(stroke),
- 'stroke': rgb2hex(stroke),
- 'stroke-width': str(rcParams['hatch.linewidth']),
- 'stroke-linecap': 'butt',
- 'stroke-linejoin': 'miter'
- })
- )
- writer.end('pattern')
- writer.end('defs')
- def _get_style_dict(self, gc, rgbFace):
- """Generate a style string from the GraphicsContext and rgbFace."""
- attrib = {}
- forced_alpha = gc.get_forced_alpha()
- if gc.get_hatch() is not None:
- attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace)
- if (rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0
- and not forced_alpha):
- attrib['fill-opacity'] = short_float_fmt(rgbFace[3])
- else:
- if rgbFace is None:
- attrib['fill'] = 'none'
- else:
- if tuple(rgbFace[:3]) != (0, 0, 0):
- attrib['fill'] = rgb2hex(rgbFace)
- if (len(rgbFace) == 4 and rgbFace[3] != 1.0
- and not forced_alpha):
- attrib['fill-opacity'] = short_float_fmt(rgbFace[3])
- if forced_alpha and gc.get_alpha() != 1.0:
- attrib['opacity'] = short_float_fmt(gc.get_alpha())
- offset, seq = gc.get_dashes()
- if seq is not None:
- attrib['stroke-dasharray'] = ','.join(
- short_float_fmt(val) for val in seq)
- attrib['stroke-dashoffset'] = short_float_fmt(float(offset))
- linewidth = gc.get_linewidth()
- if linewidth:
- rgb = gc.get_rgb()
- attrib['stroke'] = rgb2hex(rgb)
- if not forced_alpha and rgb[3] != 1.0:
- attrib['stroke-opacity'] = short_float_fmt(rgb[3])
- if linewidth != 1.0:
- attrib['stroke-width'] = short_float_fmt(linewidth)
- if gc.get_joinstyle() != 'round':
- attrib['stroke-linejoin'] = gc.get_joinstyle()
- if gc.get_capstyle() != 'butt':
- attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()]
- return attrib
- def _get_style(self, gc, rgbFace):
- return generate_css(self._get_style_dict(gc, rgbFace))
- def _get_clip(self, gc):
- cliprect = gc.get_clip_rectangle()
- clippath, clippath_trans = gc.get_clip_path()
- if clippath is not None:
- clippath_trans = self._make_flip_transform(clippath_trans)
- dictkey = (id(clippath), str(clippath_trans))
- elif cliprect is not None:
- x, y, w, h = cliprect.bounds
- y = self.height-(y+h)
- dictkey = (x, y, w, h)
- else:
- return None
- clip = self._clipd.get(dictkey)
- if clip is None:
- oid = self._make_id('p', dictkey)
- if clippath is not None:
- self._clipd[dictkey] = ((clippath, clippath_trans), oid)
- else:
- self._clipd[dictkey] = (dictkey, oid)
- else:
- clip, oid = clip
- return oid
- def _write_clips(self):
- if not len(self._clipd):
- return
- writer = self.writer
- writer.start('defs')
- for clip, oid in self._clipd.values():
- writer.start('clipPath', id=oid)
- if len(clip) == 2:
- clippath, clippath_trans = clip
- path_data = self._convert_path(
- clippath, clippath_trans, simplify=False)
- writer.element('path', d=path_data)
- else:
- x, y, w, h = clip
- writer.element(
- 'rect',
- x=short_float_fmt(x),
- y=short_float_fmt(y),
- width=short_float_fmt(w),
- height=short_float_fmt(h))
- writer.end('clipPath')
- writer.end('defs')
- def open_group(self, s, gid=None):
- # docstring inherited
- if gid:
- self.writer.start('g', id=gid)
- else:
- self._groupd[s] = self._groupd.get(s, 0) + 1
- self.writer.start('g', id="%s_%d" % (s, self._groupd[s]))
- def close_group(self, s):
- # docstring inherited
- self.writer.end('g')
- def option_image_nocomposite(self):
- # docstring inherited
- return not rcParams['image.composite_image']
- def _convert_path(self, path, transform=None, clip=None, simplify=None,
- sketch=None):
- if clip:
- clip = (0.0, 0.0, self.width, self.height)
- else:
- clip = None
- return _path.convert_to_string(
- path, transform, clip, simplify, sketch, 6,
- [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii')
- def draw_path(self, gc, path, transform, rgbFace=None):
- # docstring inherited
- trans_and_flip = self._make_flip_transform(transform)
- clip = (rgbFace is None and gc.get_hatch_path() is None)
- simplify = path.should_simplify and clip
- path_data = self._convert_path(
- path, trans_and_flip, clip=clip, simplify=simplify,
- sketch=gc.get_sketch_params())
- attrib = {}
- attrib['style'] = self._get_style(gc, rgbFace)
- clipid = self._get_clip(gc)
- if clipid is not None:
- attrib['clip-path'] = 'url(#%s)' % clipid
- if gc.get_url() is not None:
- self.writer.start('a', {'xlink:href': gc.get_url()})
- self.writer.element('path', d=path_data, attrib=attrib)
- if gc.get_url() is not None:
- self.writer.end('a')
- def draw_markers(
- self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
- # docstring inherited
- if not len(path.vertices):
- return
- writer = self.writer
- path_data = self._convert_path(
- marker_path,
- marker_trans + Affine2D().scale(1.0, -1.0),
- simplify=False)
- style = self._get_style_dict(gc, rgbFace)
- dictkey = (path_data, generate_css(style))
- oid = self._markers.get(dictkey)
- style = generate_css({k: v for k, v in style.items()
- if k.startswith('stroke')})
- if oid is None:
- oid = self._make_id('m', dictkey)
- writer.start('defs')
- writer.element('path', id=oid, d=path_data, style=style)
- writer.end('defs')
- self._markers[dictkey] = oid
- attrib = {}
- clipid = self._get_clip(gc)
- if clipid is not None:
- attrib['clip-path'] = 'url(#%s)' % clipid
- writer.start('g', attrib=attrib)
- trans_and_flip = self._make_flip_transform(trans)
- attrib = {'xlink:href': '#%s' % oid}
- clip = (0, 0, self.width*72, self.height*72)
- for vertices, code in path.iter_segments(
- trans_and_flip, clip=clip, simplify=False):
- if len(vertices):
- x, y = vertices[-2:]
- attrib['x'] = short_float_fmt(x)
- attrib['y'] = short_float_fmt(y)
- attrib['style'] = self._get_style(gc, rgbFace)
- writer.element('use', attrib=attrib)
- writer.end('g')
- def draw_path_collection(self, gc, master_transform, paths, all_transforms,
- offsets, offsetTrans, facecolors, edgecolors,
- linewidths, linestyles, antialiaseds, urls,
- offset_position):
- # Is the optimization worth it? Rough calculation:
- # cost of emitting a path in-line is
- # (len_path + 5) * uses_per_path
- # cost of definition+use is
- # (len_path + 3) + 9 * uses_per_path
- len_path = len(paths[0].vertices) if len(paths) > 0 else 0
- uses_per_path = self._iter_collection_uses_per_path(
- paths, all_transforms, offsets, facecolors, edgecolors)
- should_do_optimization = \
- len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path
- if not should_do_optimization:
- return RendererBase.draw_path_collection(
- self, gc, master_transform, paths, all_transforms,
- offsets, offsetTrans, facecolors, edgecolors,
- linewidths, linestyles, antialiaseds, urls,
- offset_position)
- writer = self.writer
- path_codes = []
- writer.start('defs')
- for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
- master_transform, paths, all_transforms)):
- transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
- d = self._convert_path(path, transform, simplify=False)
- oid = 'C%x_%x_%s' % (
- self._path_collection_id, i, self._make_id('', d))
- writer.element('path', id=oid, d=d)
- path_codes.append(oid)
- writer.end('defs')
- for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
- gc, master_transform, all_transforms, path_codes, offsets,
- offsetTrans, facecolors, edgecolors, linewidths, linestyles,
- antialiaseds, urls, offset_position):
- clipid = self._get_clip(gc0)
- url = gc0.get_url()
- if url is not None:
- writer.start('a', attrib={'xlink:href': url})
- if clipid is not None:
- writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})
- attrib = {
- 'xlink:href': '#%s' % path_id,
- 'x': short_float_fmt(xo),
- 'y': short_float_fmt(self.height - yo),
- 'style': self._get_style(gc0, rgbFace)
- }
- writer.element('use', attrib=attrib)
- if clipid is not None:
- writer.end('g')
- if url is not None:
- writer.end('a')
- self._path_collection_id += 1
- def draw_gouraud_triangle(self, gc, points, colors, trans):
- # This uses a method described here:
- #
- # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
- #
- # that uses three overlapping linear gradients to simulate a
- # Gouraud triangle. Each gradient goes from fully opaque in
- # one corner to fully transparent along the opposite edge.
- # The line between the stop points is perpendicular to the
- # opposite edge. Underlying these three gradients is a solid
- # triangle whose color is the average of all three points.
- writer = self.writer
- if not self._has_gouraud:
- self._has_gouraud = True
- writer.start(
- 'filter',
- id='colorAdd')
- writer.element(
- 'feComposite',
- attrib={'in': 'SourceGraphic'},
- in2='BackgroundImage',
- operator='arithmetic',
- k2="1", k3="1")
- writer.end('filter')
- # feColorMatrix filter to correct opacity
- writer.start(
- 'filter',
- id='colorMat')
- writer.element(
- 'feColorMatrix',
- attrib={'type': 'matrix'},
- values='1 0 0 0 0 \n0 1 0 0 0 \n0 0 1 0 0' +
- ' \n1 1 1 1 0 \n0 0 0 0 1 ')
- writer.end('filter')
- avg_color = np.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),
- gradientUnits="userSpaceOnUse",
- x1=short_float_fmt(x1), y1=short_float_fmt(y1),
- x2=short_float_fmt(xb), y2=short_float_fmt(yb))
- writer.element(
- 'stop',
- offset='1',
- style=generate_css({'stop-color': rgb2hex(avg_color),
- 'stop-opacity': short_float_fmt(c[-1])}))
- writer.element(
- 'stop',
- offset='0',
- style=generate_css({'stop-color': rgb2hex(c),
- 'stop-opacity': "0"}))
- writer.end('linearGradient')
- writer.end('defs')
- # triangle formation using "path"
- dpath = "M " + short_float_fmt(x1)+',' + short_float_fmt(y1)
- dpath += " L " + short_float_fmt(x2) + ',' + short_float_fmt(y2)
- dpath += " " + short_float_fmt(x3) + ',' + short_float_fmt(y3) + " Z"
- writer.element(
- 'path',
- attrib={'d': dpath,
- 'fill': rgb2hex(avg_color),
- 'fill-opacity': '1',
- 'shape-rendering': "crispEdges"})
- writer.start(
- 'g',
- attrib={'stroke': "none",
- 'stroke-width': "0",
- 'shape-rendering': "crispEdges",
- 'filter': "url(#colorMat)"})
- writer.element(
- 'path',
- attrib={'d': dpath,
- 'fill': 'url(#GR%x_0)' % self._n_gradients,
- 'shape-rendering': "crispEdges"})
- writer.element(
- 'path',
- attrib={'d': dpath,
- 'fill': 'url(#GR%x_1)' % self._n_gradients,
- 'filter': 'url(#colorAdd)',
- 'shape-rendering': "crispEdges"})
- writer.element(
- 'path',
- attrib={'d': dpath,
- 'fill': 'url(#GR%x_2)' % self._n_gradients,
- 'filter': 'url(#colorAdd)',
- 'shape-rendering': "crispEdges"})
- writer.end('g')
- self._n_gradients += 1
- def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
- transform):
- attrib = {}
- clipid = self._get_clip(gc)
- if clipid is not None:
- attrib['clip-path'] = 'url(#%s)' % clipid
- self.writer.start('g', attrib=attrib)
- transform = transform.frozen()
- for tri, col in zip(triangles_array, colors_array):
- self.draw_gouraud_triangle(gc, tri, col, transform)
- self.writer.end('g')
- def option_scale_image(self):
- # docstring inherited
- return True
- def get_image_magnification(self):
- return self.image_dpi / 72.0
- def draw_image(self, gc, x, y, im, transform=None):
- # docstring inherited
- h, w = im.shape[:2]
- if w == 0 or h == 0:
- return
- attrib = {}
- clipid = self._get_clip(gc)
- if clipid is not None:
- # Can't apply clip-path directly to the image because the
- # image has a transformation, which would also be applied
- # to the clip-path
- self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})
- oid = gc.get_gid()
- url = gc.get_url()
- if url is not None:
- self.writer.start('a', attrib={'xlink:href': url})
- if rcParams['svg.image_inline']:
- buf = _png.write_png(im, None)
- oid = oid or self._make_id('image', buf)
- attrib['xlink:href'] = (
- "data:image/png;base64,\n" +
- base64.b64encode(buf).decode('ascii'))
- else:
- if self.basename is None:
- raise ValueError("Cannot save image data to filesystem when "
- "writing SVG to an in-memory buffer")
- filename = '{}.image{}.png'.format(
- self.basename, next(self._image_counter))
- _log.info('Writing image file for inclusion: %s', filename)
- with open(filename, 'wb') as file:
- _png.write_png(im, file)
- oid = oid or 'Im_' + self._make_id('image', filename)
- attrib['xlink:href'] = filename
- attrib['id'] = oid
- if transform is None:
- w = 72.0 * w / self.image_dpi
- h = 72.0 * h / self.image_dpi
- self.writer.element(
- 'image',
- transform=generate_transform([
- ('scale', (1, -1)), ('translate', (0, -h))]),
- x=short_float_fmt(x),
- y=short_float_fmt(-(self.height - y - h)),
- width=short_float_fmt(w), height=short_float_fmt(h),
- attrib=attrib)
- else:
- alpha = gc.get_alpha()
- if alpha != 1.0:
- attrib['opacity'] = short_float_fmt(alpha)
- flipped = (
- Affine2D().scale(1.0 / w, 1.0 / h) +
- transform +
- Affine2D()
- .translate(x, y)
- .scale(1.0, -1.0)
- .translate(0.0, self.height))
- attrib['transform'] = generate_transform(
- [('matrix', flipped.frozen())])
- self.writer.element(
- 'image',
- width=short_float_fmt(w), height=short_float_fmt(h),
- attrib=attrib)
- if url is not None:
- self.writer.end('a')
- if clipid is not None:
- self.writer.end('g')
- def _adjust_char_id(self, char_id):
- return char_id.replace("%20", "_")
- def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
- """
- draw the text by converting them to paths using textpath module.
- Parameters
- ----------
- prop : `matplotlib.font_manager.FontProperties`
- font property
- s : str
- text to be converted
- usetex : bool
- If True, use matplotlib usetex mode.
- ismath : bool
- If True, use mathtext parser. If "TeX", use *usetex* mode.
- """
- writer = self.writer
- writer.comment(s)
- glyph_map = self._glyph_map
- text2path = self._text2path
- color = rgb2hex(gc.get_rgb())
- fontsize = prop.get_size_in_points()
- style = {}
- if color != '#000000':
- style['fill'] = color
- alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
- if alpha != 1:
- style['opacity'] = short_float_fmt(alpha)
- if not ismath:
- font = 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 glyph_map_new.items():
- 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 these characters are used.
- if glyph_map_new:
- writer.start('defs')
- for char_id, glyph_path in glyph_map_new.items():
- 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
- alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
- if alpha != 1:
- style['opacity'] = short_float_fmt(alpha)
- if not ismath:
- font = self._get_font(prop)
- font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
- attrib = {}
- # Must add "px" to workaround a Firefox bug
- style['font-size'] = short_float_fmt(prop.get_size()) + 'px'
- style['font-family'] = str(font.family_name)
- style['font-style'] = prop.get_style().lower()
- style['font-weight'] = str(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(mtext.get_unitless_position())
- ay = self.height - ay
- # Don't do vertical anchor alignment. Most applications do not
- # support 'alignment-baseline' yet. Apply the vertical layout
- # to the anchor point manually for now.
- angle_rad = np.deg2rad(angle)
- dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)])
- v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
- ax = ax + v_offset * dir_vert[0]
- ay = ay + v_offset * dir_vert[1]
- ha_mpl_to_svg = {'left': 'start', 'right': 'end',
- 'center': 'middle'}
- style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()]
- attrib['x'] = short_float_fmt(ax)
- attrib['y'] = short_float_fmt(ay)
- attrib['style'] = generate_css(style)
- attrib['transform'] = "rotate(%s, %s, %s)" % (
- short_float_fmt(-angle),
- short_float_fmt(ax),
- short_float_fmt(ay))
- writer.element('text', s, attrib=attrib)
- else:
- attrib['transform'] = generate_transform([
- ('translate', (x, y)),
- ('rotate', (-angle,))])
- writer.element('text', s, attrib=attrib)
- else:
- writer.comment(s)
- width, height, descent, svg_elements, used_characters = \
- self.mathtext_parser.parse(s, 72, prop)
- svg_glyphs = svg_elements.svg_glyphs
- svg_rects = svg_elements.svg_rects
- attrib = {}
- attrib['style'] = generate_css(style)
- attrib['transform'] = generate_transform([
- ('translate', (x, y)),
- ('rotate', (-angle,))])
- # Apply attributes to 'g', not 'text', because we likely have some
- # rectangles as well with the same style and transformation.
- writer.start('g', attrib=attrib)
- writer.start('text')
- # Sort the characters by font, and output one tspan for each.
- spans = OrderedDict()
- for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
- style = generate_css({
- 'font-size': short_float_fmt(fontsize) + 'px',
- 'font-family': font.family_name,
- 'font-style': font.style_name.lower(),
- 'font-weight': font.style_name.lower()})
- if thetext == 32:
- thetext = 0xa0 # non-breaking space
- spans.setdefault(style, []).append((new_x, -new_y, thetext))
- for style, chars in spans.items():
- chars.sort()
- if len({y for x, y, t in chars}) == 1: # Are all y's the same?
- ys = str(chars[0][1])
- else:
- ys = ' '.join(str(c[1]) for c in chars)
- attrib = {
- 'style': style,
- 'x': ' '.join(short_float_fmt(c[0]) for c in chars),
- 'y': ys
- }
- writer.element(
- 'tspan',
- ''.join(chr(c[2]) for c in chars),
- attrib=attrib)
- writer.end('text')
- if len(svg_rects):
- for x, y, width, height in svg_rects:
- writer.element(
- 'rect',
- x=short_float_fmt(x),
- y=short_float_fmt(-y + height),
- width=short_float_fmt(width),
- height=short_float_fmt(height)
- )
- writer.end('g')
- def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
- # docstring inherited
- self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")
- def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
- # docstring inherited
- clipid = self._get_clip(gc)
- if clipid is not None:
- # Cannot apply clip-path directly to the text, because
- # is has a transformation
- self.writer.start(
- 'g', attrib={'clip-path': 'url(#%s)' % clipid})
- if gc.get_url() is not None:
- self.writer.start('a', {'xlink:href': gc.get_url()})
- if rcParams['svg.fonttype'] == 'path':
- self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext)
- else:
- self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext)
- if gc.get_url() is not None:
- self.writer.end('a')
- if clipid is not None:
- self.writer.end('g')
- def flipy(self):
- # docstring inherited
- return True
- def get_canvas_width_height(self):
- # docstring inherited
- return self.width, self.height
- def get_text_width_height_descent(self, s, prop, ismath):
- # docstring inherited
- return self._text2path.get_text_width_height_descent(s, prop, ismath)
- class FigureCanvasSVG(FigureCanvasBase):
- filetypes = {'svg': 'Scalable Vector Graphics',
- 'svgz': 'Scalable Vector Graphics'}
- fixed_dpi = 72
- def print_svg(self, filename, *args, **kwargs):
- with cbook.open_file_cm(filename, "w", encoding="utf-8") as fh:
- filename = getattr(fh, 'name', '')
- if not isinstance(filename, str):
- filename = ''
- if cbook.file_requires_unicode(fh):
- detach = False
- else:
- fh = io.TextIOWrapper(fh, 'utf-8')
- detach = True
- result = self._print_svg(filename, fh, **kwargs)
- # Detach underlying stream from wrapper so that it remains open in
- # the caller.
- if detach:
- fh.detach()
- return result
- def print_svgz(self, filename, *args, **kwargs):
- with cbook.open_file_cm(filename, "wb") as fh, \
- gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter:
- return self.print_svg(gzipwriter)
- def _print_svg(
- self, filename, fh, *, dpi=72, bbox_inches_restore=None, **kwargs):
- self.figure.set_dpi(72.0)
- width, height = self.figure.get_size_inches()
- w, h = width * 72, height * 72
- renderer = MixedModeRenderer(
- self.figure, width, height, dpi,
- RendererSVG(w, h, fh, filename, dpi),
- bbox_inches_restore=bbox_inches_restore)
- self.figure.draw(renderer)
- renderer.finalize()
- def get_default_filetype(self):
- return 'svg'
- FigureManagerSVG = FigureManagerBase
- svgProlog = """\
- <?xml version="1.0" encoding="utf-8" standalone="no"?>
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
- "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
- <!-- Created with matplotlib (https://matplotlib.org/) -->
- """
- @_Backend.export
- class _BackendSVG(_Backend):
- FigureCanvas = FigureCanvasSVG
|