backend_svg.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373
  1. from collections import OrderedDict
  2. import base64
  3. import datetime
  4. import gzip
  5. import hashlib
  6. from io import BytesIO, StringIO, TextIOWrapper
  7. import itertools
  8. import logging
  9. import os
  10. import re
  11. import uuid
  12. import numpy as np
  13. from PIL import Image
  14. import matplotlib as mpl
  15. from matplotlib import cbook
  16. from matplotlib.backend_bases import (
  17. _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
  18. RendererBase)
  19. from matplotlib.backends.backend_mixed import MixedModeRenderer
  20. from matplotlib.colors import rgb2hex
  21. from matplotlib.dates import UTC
  22. from matplotlib.font_manager import findfont, get_font
  23. from matplotlib.ft2font import LOAD_NO_HINTING
  24. from matplotlib.mathtext import MathTextParser
  25. from matplotlib.path import Path
  26. from matplotlib import _path
  27. from matplotlib.transforms import Affine2D, Affine2DBase
  28. _log = logging.getLogger(__name__)
  29. backend_version = mpl.__version__
  30. # ----------------------------------------------------------------------
  31. # SimpleXMLWriter class
  32. #
  33. # Based on an original by Fredrik Lundh, but modified here to:
  34. # 1. Support modern Python idioms
  35. # 2. Remove encoding support (it's handled by the file writer instead)
  36. # 3. Support proper indentation
  37. # 4. Minify things a little bit
  38. # --------------------------------------------------------------------
  39. # The SimpleXMLWriter module is
  40. #
  41. # Copyright (c) 2001-2004 by Fredrik Lundh
  42. #
  43. # By obtaining, using, and/or copying this software and/or its
  44. # associated documentation, you agree that you have read, understood,
  45. # and will comply with the following terms and conditions:
  46. #
  47. # Permission to use, copy, modify, and distribute this software and
  48. # its associated documentation for any purpose and without fee is
  49. # hereby granted, provided that the above copyright notice appears in
  50. # all copies, and that both that copyright notice and this permission
  51. # notice appear in supporting documentation, and that the name of
  52. # Secret Labs AB or the author not be used in advertising or publicity
  53. # pertaining to distribution of the software without specific, written
  54. # prior permission.
  55. #
  56. # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
  57. # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
  58. # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
  59. # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  60. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  61. # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
  62. # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
  63. # OF THIS SOFTWARE.
  64. # --------------------------------------------------------------------
  65. def escape_cdata(s):
  66. s = s.replace("&", "&")
  67. s = s.replace("<", "&lt;")
  68. s = s.replace(">", "&gt;")
  69. return s
  70. _escape_xml_comment = re.compile(r'-(?=-)')
  71. def escape_comment(s):
  72. s = escape_cdata(s)
  73. return _escape_xml_comment.sub('- ', s)
  74. def escape_attrib(s):
  75. s = s.replace("&", "&amp;")
  76. s = s.replace("'", "&apos;")
  77. s = s.replace('"', "&quot;")
  78. s = s.replace("<", "&lt;")
  79. s = s.replace(">", "&gt;")
  80. return s
  81. def short_float_fmt(x):
  82. """
  83. Create a short string representation of a float, which is %f
  84. formatting with trailing zeros and the decimal point removed.
  85. """
  86. return '{0:f}'.format(x).rstrip('0').rstrip('.')
  87. class XMLWriter:
  88. """
  89. Parameters
  90. ----------
  91. file : writable text file-like object
  92. """
  93. def __init__(self, file):
  94. self.__write = file.write
  95. if hasattr(file, "flush"):
  96. self.flush = file.flush
  97. self.__open = 0 # true if start tag is open
  98. self.__tags = []
  99. self.__data = []
  100. self.__indentation = " " * 64
  101. def __flush(self, indent=True):
  102. # flush internal buffers
  103. if self.__open:
  104. if indent:
  105. self.__write(">\n")
  106. else:
  107. self.__write(">")
  108. self.__open = 0
  109. if self.__data:
  110. data = ''.join(self.__data)
  111. self.__write(escape_cdata(data))
  112. self.__data = []
  113. def start(self, tag, attrib={}, **extra):
  114. """
  115. Open a new element. Attributes can be given as keyword
  116. arguments, or as a string/string dictionary. The method returns
  117. an opaque identifier that can be passed to the :meth:`close`
  118. method, to close all open elements up to and including this one.
  119. Parameters
  120. ----------
  121. tag
  122. Element tag.
  123. attrib
  124. Attribute dictionary. Alternatively, attributes can be given as
  125. keyword arguments.
  126. Returns
  127. -------
  128. An element identifier.
  129. """
  130. self.__flush()
  131. tag = escape_cdata(tag)
  132. self.__data = []
  133. self.__tags.append(tag)
  134. self.__write(self.__indentation[:len(self.__tags) - 1])
  135. self.__write("<%s" % tag)
  136. for k, v in sorted({**attrib, **extra}.items()):
  137. if v:
  138. k = escape_cdata(k)
  139. v = escape_attrib(v)
  140. self.__write(' %s="%s"' % (k, v))
  141. self.__open = 1
  142. return len(self.__tags) - 1
  143. def comment(self, comment):
  144. """
  145. Add a comment to the output stream.
  146. Parameters
  147. ----------
  148. comment : str
  149. Comment text.
  150. """
  151. self.__flush()
  152. self.__write(self.__indentation[:len(self.__tags)])
  153. self.__write("<!-- %s -->\n" % escape_comment(comment))
  154. def data(self, text):
  155. """
  156. Add character data to the output stream.
  157. Parameters
  158. ----------
  159. text : str
  160. Character data.
  161. """
  162. self.__data.append(text)
  163. def end(self, tag=None, indent=True):
  164. """
  165. Close the current element (opened by the most recent call to
  166. :meth:`start`).
  167. Parameters
  168. ----------
  169. tag
  170. Element tag. If given, the tag must match the start tag. If
  171. omitted, the current element is closed.
  172. """
  173. if tag:
  174. assert self.__tags, "unbalanced end(%s)" % tag
  175. assert escape_cdata(tag) == self.__tags[-1], \
  176. "expected end(%s), got %s" % (self.__tags[-1], tag)
  177. else:
  178. assert self.__tags, "unbalanced end()"
  179. tag = self.__tags.pop()
  180. if self.__data:
  181. self.__flush(indent)
  182. elif self.__open:
  183. self.__open = 0
  184. self.__write("/>\n")
  185. return
  186. if indent:
  187. self.__write(self.__indentation[:len(self.__tags)])
  188. self.__write("</%s>\n" % tag)
  189. def close(self, id):
  190. """
  191. Close open elements, up to (and including) the element identified
  192. by the given identifier.
  193. Parameters
  194. ----------
  195. id
  196. Element identifier, as returned by the :meth:`start` method.
  197. """
  198. while len(self.__tags) > id:
  199. self.end()
  200. def element(self, tag, text=None, attrib={}, **extra):
  201. """
  202. Add an entire element. This is the same as calling :meth:`start`,
  203. :meth:`data`, and :meth:`end` in sequence. The *text* argument can be
  204. omitted.
  205. """
  206. self.start(tag, attrib, **extra)
  207. if text:
  208. self.data(text)
  209. self.end(indent=False)
  210. def flush(self):
  211. """Flush the output stream."""
  212. pass # replaced by the constructor
  213. def generate_transform(transform_list=[]):
  214. if len(transform_list):
  215. output = StringIO()
  216. for type, value in transform_list:
  217. if (type == 'scale' and (value == (1,) or value == (1, 1))
  218. or type == 'translate' and value == (0, 0)
  219. or type == 'rotate' and value == (0,)):
  220. continue
  221. if type == 'matrix' and isinstance(value, Affine2DBase):
  222. value = value.to_values()
  223. output.write('%s(%s)' % (
  224. type, ' '.join(short_float_fmt(x) for x in value)))
  225. return output.getvalue()
  226. return ''
  227. def generate_css(attrib={}):
  228. if attrib:
  229. output = StringIO()
  230. attrib = sorted(attrib.items())
  231. for k, v in attrib:
  232. k = escape_attrib(k)
  233. v = escape_attrib(v)
  234. output.write("%s:%s;" % (k, v))
  235. return output.getvalue()
  236. return ''
  237. _capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'}
  238. class RendererSVG(RendererBase):
  239. def __init__(self, width, height, svgwriter, basename=None, image_dpi=72,
  240. *, metadata=None):
  241. self.width = width
  242. self.height = height
  243. self.writer = XMLWriter(svgwriter)
  244. self.image_dpi = image_dpi # actual dpi at which we rasterize stuff
  245. self._groupd = {}
  246. self.basename = basename
  247. self._image_counter = itertools.count()
  248. self._clipd = OrderedDict()
  249. self._markers = {}
  250. self._path_collection_id = 0
  251. self._hatchd = OrderedDict()
  252. self._has_gouraud = False
  253. self._n_gradients = 0
  254. self._fonts = OrderedDict()
  255. self.mathtext_parser = MathTextParser('SVG')
  256. RendererBase.__init__(self)
  257. self._glyph_map = dict()
  258. str_height = short_float_fmt(height)
  259. str_width = short_float_fmt(width)
  260. svgwriter.write(svgProlog)
  261. self._start_id = self.writer.start(
  262. 'svg',
  263. width='%spt' % str_width,
  264. height='%spt' % str_height,
  265. viewBox='0 0 %s %s' % (str_width, str_height),
  266. xmlns="http://www.w3.org/2000/svg",
  267. version="1.1",
  268. attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"})
  269. self._write_metadata(metadata)
  270. self._write_default_style()
  271. def finalize(self):
  272. self._write_clips()
  273. self._write_hatches()
  274. self.writer.close(self._start_id)
  275. self.writer.flush()
  276. def _write_metadata(self, metadata):
  277. # Add metadata following the Dublin Core Metadata Initiative, and the
  278. # Creative Commons Rights Expression Language. This is mainly for
  279. # compatibility with Inkscape.
  280. if metadata is None:
  281. metadata = {}
  282. metadata = {
  283. 'Format': 'image/svg+xml',
  284. 'Type': 'http://purl.org/dc/dcmitype/StillImage',
  285. 'Creator':
  286. f'Matplotlib v{mpl.__version__}, https://matplotlib.org/',
  287. **metadata
  288. }
  289. writer = self.writer
  290. if 'Title' in metadata:
  291. writer.element('title', text=metadata['Title'])
  292. # Special handling.
  293. date = metadata.get('Date', None)
  294. if date is not None:
  295. if isinstance(date, str):
  296. dates = [date]
  297. elif isinstance(date, (datetime.datetime, datetime.date)):
  298. dates = [date.isoformat()]
  299. elif np.iterable(date):
  300. dates = []
  301. for d in date:
  302. if isinstance(d, str):
  303. dates.append(d)
  304. elif isinstance(d, (datetime.datetime, datetime.date)):
  305. dates.append(d.isoformat())
  306. else:
  307. raise ValueError(
  308. 'Invalid type for Date metadata. '
  309. 'Expected iterable of str, date, or datetime, '
  310. 'not {!r}.'.format(type(d)))
  311. else:
  312. raise ValueError('Invalid type for Date metadata. '
  313. 'Expected str, date, datetime, or iterable '
  314. 'of the same, not {!r}.'.format(type(date)))
  315. metadata['Date'] = '/'.join(dates)
  316. elif 'Date' not in metadata:
  317. # Do not add `Date` if the user explicitly set `Date` to `None`
  318. # Get source date from SOURCE_DATE_EPOCH, if set.
  319. # See https://reproducible-builds.org/specs/source-date-epoch/
  320. date = os.getenv("SOURCE_DATE_EPOCH")
  321. if date:
  322. date = datetime.datetime.utcfromtimestamp(int(date))
  323. metadata['Date'] = date.replace(tzinfo=UTC).isoformat()
  324. else:
  325. metadata['Date'] = datetime.datetime.today().isoformat()
  326. mid = None
  327. def ensure_metadata(mid):
  328. if mid is not None:
  329. return mid
  330. mid = writer.start('metadata')
  331. writer.start('rdf:RDF', attrib={
  332. 'xmlns:dc': "http://purl.org/dc/elements/1.1/",
  333. 'xmlns:cc': "http://creativecommons.org/ns#",
  334. 'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  335. })
  336. writer.start('cc:Work')
  337. return mid
  338. uri = metadata.pop('Type', None)
  339. if uri is not None:
  340. mid = ensure_metadata(mid)
  341. writer.element('dc:type', attrib={'rdf:resource': uri})
  342. # Single value only.
  343. for key in ['title', 'coverage', 'date', 'description', 'format',
  344. 'identifier', 'language', 'relation', 'source']:
  345. info = metadata.pop(key.title(), None)
  346. if info is not None:
  347. mid = ensure_metadata(mid)
  348. writer.element(f'dc:{key}', text=info)
  349. # Multiple Agent values.
  350. for key in ['creator', 'contributor', 'publisher', 'rights']:
  351. agents = metadata.pop(key.title(), None)
  352. if agents is None:
  353. continue
  354. if isinstance(agents, str):
  355. agents = [agents]
  356. mid = ensure_metadata(mid)
  357. writer.start(f'dc:{key}')
  358. for agent in agents:
  359. writer.start('cc:Agent')
  360. writer.element('dc:title', text=agent)
  361. writer.end('cc:Agent')
  362. writer.end(f'dc:{key}')
  363. # Multiple values.
  364. keywords = metadata.pop('Keywords', None)
  365. if keywords is not None:
  366. if isinstance(keywords, str):
  367. keywords = [keywords]
  368. mid = ensure_metadata(mid)
  369. writer.start('dc:subject')
  370. writer.start('rdf:Bag')
  371. for keyword in keywords:
  372. writer.element('rdf:li', text=keyword)
  373. writer.end('rdf:Bag')
  374. writer.end('dc:subject')
  375. if mid is not None:
  376. writer.close(mid)
  377. if metadata:
  378. raise ValueError('Unknown metadata key(s) passed to SVG writer: ' +
  379. ','.join(metadata))
  380. def _write_default_style(self):
  381. writer = self.writer
  382. default_style = generate_css({
  383. 'stroke-linejoin': 'round',
  384. 'stroke-linecap': 'butt'})
  385. writer.start('defs')
  386. writer.element('style', type='text/css', text='*{%s}' % default_style)
  387. writer.end('defs')
  388. def _make_id(self, type, content):
  389. salt = mpl.rcParams['svg.hashsalt']
  390. if salt is None:
  391. salt = str(uuid.uuid4())
  392. m = hashlib.md5()
  393. m.update(salt.encode('utf8'))
  394. m.update(str(content).encode('utf8'))
  395. return '%s%s' % (type, m.hexdigest()[:10])
  396. def _make_flip_transform(self, transform):
  397. return (transform +
  398. Affine2D()
  399. .scale(1.0, -1.0)
  400. .translate(0.0, self.height))
  401. def _get_font(self, prop):
  402. fname = findfont(prop)
  403. font = get_font(fname)
  404. font.clear()
  405. size = prop.get_size_in_points()
  406. font.set_size(size, 72.0)
  407. return font
  408. def _get_hatch(self, gc, rgbFace):
  409. """
  410. Create a new hatch pattern
  411. """
  412. if rgbFace is not None:
  413. rgbFace = tuple(rgbFace)
  414. edge = gc.get_hatch_color()
  415. if edge is not None:
  416. edge = tuple(edge)
  417. dictkey = (gc.get_hatch(), rgbFace, edge)
  418. oid = self._hatchd.get(dictkey)
  419. if oid is None:
  420. oid = self._make_id('h', dictkey)
  421. self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid)
  422. else:
  423. _, oid = oid
  424. return oid
  425. def _write_hatches(self):
  426. if not len(self._hatchd):
  427. return
  428. HATCH_SIZE = 72
  429. writer = self.writer
  430. writer.start('defs')
  431. for (path, face, stroke), oid in self._hatchd.values():
  432. writer.start(
  433. 'pattern',
  434. id=oid,
  435. patternUnits="userSpaceOnUse",
  436. x="0", y="0", width=str(HATCH_SIZE),
  437. height=str(HATCH_SIZE))
  438. path_data = self._convert_path(
  439. path,
  440. Affine2D()
  441. .scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE),
  442. simplify=False)
  443. if face is None:
  444. fill = 'none'
  445. else:
  446. fill = rgb2hex(face)
  447. writer.element(
  448. 'rect',
  449. x="0", y="0", width=str(HATCH_SIZE+1),
  450. height=str(HATCH_SIZE+1),
  451. fill=fill)
  452. hatch_style = {
  453. 'fill': rgb2hex(stroke),
  454. 'stroke': rgb2hex(stroke),
  455. 'stroke-width': str(mpl.rcParams['hatch.linewidth']),
  456. 'stroke-linecap': 'butt',
  457. 'stroke-linejoin': 'miter'
  458. }
  459. if stroke[3] < 1:
  460. hatch_style['stroke-opacity'] = str(stroke[3])
  461. writer.element(
  462. 'path',
  463. d=path_data,
  464. style=generate_css(hatch_style)
  465. )
  466. writer.end('pattern')
  467. writer.end('defs')
  468. def _get_style_dict(self, gc, rgbFace):
  469. """Generate a style string from the GraphicsContext and rgbFace."""
  470. attrib = {}
  471. forced_alpha = gc.get_forced_alpha()
  472. if gc.get_hatch() is not None:
  473. attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace)
  474. if (rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0
  475. and not forced_alpha):
  476. attrib['fill-opacity'] = short_float_fmt(rgbFace[3])
  477. else:
  478. if rgbFace is None:
  479. attrib['fill'] = 'none'
  480. else:
  481. if tuple(rgbFace[:3]) != (0, 0, 0):
  482. attrib['fill'] = rgb2hex(rgbFace)
  483. if (len(rgbFace) == 4 and rgbFace[3] != 1.0
  484. and not forced_alpha):
  485. attrib['fill-opacity'] = short_float_fmt(rgbFace[3])
  486. if forced_alpha and gc.get_alpha() != 1.0:
  487. attrib['opacity'] = short_float_fmt(gc.get_alpha())
  488. offset, seq = gc.get_dashes()
  489. if seq is not None:
  490. attrib['stroke-dasharray'] = ','.join(
  491. short_float_fmt(val) for val in seq)
  492. attrib['stroke-dashoffset'] = short_float_fmt(float(offset))
  493. linewidth = gc.get_linewidth()
  494. if linewidth:
  495. rgb = gc.get_rgb()
  496. attrib['stroke'] = rgb2hex(rgb)
  497. if not forced_alpha and rgb[3] != 1.0:
  498. attrib['stroke-opacity'] = short_float_fmt(rgb[3])
  499. if linewidth != 1.0:
  500. attrib['stroke-width'] = short_float_fmt(linewidth)
  501. if gc.get_joinstyle() != 'round':
  502. attrib['stroke-linejoin'] = gc.get_joinstyle()
  503. if gc.get_capstyle() != 'butt':
  504. attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()]
  505. return attrib
  506. def _get_style(self, gc, rgbFace):
  507. return generate_css(self._get_style_dict(gc, rgbFace))
  508. def _get_clip(self, gc):
  509. cliprect = gc.get_clip_rectangle()
  510. clippath, clippath_trans = gc.get_clip_path()
  511. if clippath is not None:
  512. clippath_trans = self._make_flip_transform(clippath_trans)
  513. dictkey = (id(clippath), str(clippath_trans))
  514. elif cliprect is not None:
  515. x, y, w, h = cliprect.bounds
  516. y = self.height-(y+h)
  517. dictkey = (x, y, w, h)
  518. else:
  519. return None
  520. clip = self._clipd.get(dictkey)
  521. if clip is None:
  522. oid = self._make_id('p', dictkey)
  523. if clippath is not None:
  524. self._clipd[dictkey] = ((clippath, clippath_trans), oid)
  525. else:
  526. self._clipd[dictkey] = (dictkey, oid)
  527. else:
  528. clip, oid = clip
  529. return oid
  530. def _write_clips(self):
  531. if not len(self._clipd):
  532. return
  533. writer = self.writer
  534. writer.start('defs')
  535. for clip, oid in self._clipd.values():
  536. writer.start('clipPath', id=oid)
  537. if len(clip) == 2:
  538. clippath, clippath_trans = clip
  539. path_data = self._convert_path(
  540. clippath, clippath_trans, simplify=False)
  541. writer.element('path', d=path_data)
  542. else:
  543. x, y, w, h = clip
  544. writer.element(
  545. 'rect',
  546. x=short_float_fmt(x),
  547. y=short_float_fmt(y),
  548. width=short_float_fmt(w),
  549. height=short_float_fmt(h))
  550. writer.end('clipPath')
  551. writer.end('defs')
  552. def open_group(self, s, gid=None):
  553. # docstring inherited
  554. if gid:
  555. self.writer.start('g', id=gid)
  556. else:
  557. self._groupd[s] = self._groupd.get(s, 0) + 1
  558. self.writer.start('g', id="%s_%d" % (s, self._groupd[s]))
  559. def close_group(self, s):
  560. # docstring inherited
  561. self.writer.end('g')
  562. def option_image_nocomposite(self):
  563. # docstring inherited
  564. return not mpl.rcParams['image.composite_image']
  565. def _convert_path(self, path, transform=None, clip=None, simplify=None,
  566. sketch=None):
  567. if clip:
  568. clip = (0.0, 0.0, self.width, self.height)
  569. else:
  570. clip = None
  571. return _path.convert_to_string(
  572. path, transform, clip, simplify, sketch, 6,
  573. [b'M', b'L', b'Q', b'C', b'z'], False).decode('ascii')
  574. def draw_path(self, gc, path, transform, rgbFace=None):
  575. # docstring inherited
  576. trans_and_flip = self._make_flip_transform(transform)
  577. clip = (rgbFace is None and gc.get_hatch_path() is None)
  578. simplify = path.should_simplify and clip
  579. path_data = self._convert_path(
  580. path, trans_and_flip, clip=clip, simplify=simplify,
  581. sketch=gc.get_sketch_params())
  582. attrib = {}
  583. attrib['style'] = self._get_style(gc, rgbFace)
  584. clipid = self._get_clip(gc)
  585. if clipid is not None:
  586. attrib['clip-path'] = 'url(#%s)' % clipid
  587. if gc.get_url() is not None:
  588. self.writer.start('a', {'xlink:href': gc.get_url()})
  589. self.writer.element('path', d=path_data, attrib=attrib)
  590. if gc.get_url() is not None:
  591. self.writer.end('a')
  592. def draw_markers(
  593. self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
  594. # docstring inherited
  595. if not len(path.vertices):
  596. return
  597. writer = self.writer
  598. path_data = self._convert_path(
  599. marker_path,
  600. marker_trans + Affine2D().scale(1.0, -1.0),
  601. simplify=False)
  602. style = self._get_style_dict(gc, rgbFace)
  603. dictkey = (path_data, generate_css(style))
  604. oid = self._markers.get(dictkey)
  605. style = generate_css({k: v for k, v in style.items()
  606. if k.startswith('stroke')})
  607. if oid is None:
  608. oid = self._make_id('m', dictkey)
  609. writer.start('defs')
  610. writer.element('path', id=oid, d=path_data, style=style)
  611. writer.end('defs')
  612. self._markers[dictkey] = oid
  613. attrib = {}
  614. clipid = self._get_clip(gc)
  615. if clipid is not None:
  616. attrib['clip-path'] = 'url(#%s)' % clipid
  617. writer.start('g', attrib=attrib)
  618. trans_and_flip = self._make_flip_transform(trans)
  619. attrib = {'xlink:href': '#%s' % oid}
  620. clip = (0, 0, self.width*72, self.height*72)
  621. for vertices, code in path.iter_segments(
  622. trans_and_flip, clip=clip, simplify=False):
  623. if len(vertices):
  624. x, y = vertices[-2:]
  625. attrib['x'] = short_float_fmt(x)
  626. attrib['y'] = short_float_fmt(y)
  627. attrib['style'] = self._get_style(gc, rgbFace)
  628. writer.element('use', attrib=attrib)
  629. writer.end('g')
  630. def draw_path_collection(self, gc, master_transform, paths, all_transforms,
  631. offsets, offsetTrans, facecolors, edgecolors,
  632. linewidths, linestyles, antialiaseds, urls,
  633. offset_position):
  634. # Is the optimization worth it? Rough calculation:
  635. # cost of emitting a path in-line is
  636. # (len_path + 5) * uses_per_path
  637. # cost of definition+use is
  638. # (len_path + 3) + 9 * uses_per_path
  639. len_path = len(paths[0].vertices) if len(paths) > 0 else 0
  640. uses_per_path = self._iter_collection_uses_per_path(
  641. paths, all_transforms, offsets, facecolors, edgecolors)
  642. should_do_optimization = \
  643. len_path + 9 * uses_per_path + 3 < (len_path + 5) * uses_per_path
  644. if not should_do_optimization:
  645. return RendererBase.draw_path_collection(
  646. self, gc, master_transform, paths, all_transforms,
  647. offsets, offsetTrans, facecolors, edgecolors,
  648. linewidths, linestyles, antialiaseds, urls,
  649. offset_position)
  650. writer = self.writer
  651. path_codes = []
  652. writer.start('defs')
  653. for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
  654. master_transform, paths, all_transforms)):
  655. transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
  656. d = self._convert_path(path, transform, simplify=False)
  657. oid = 'C%x_%x_%s' % (
  658. self._path_collection_id, i, self._make_id('', d))
  659. writer.element('path', id=oid, d=d)
  660. path_codes.append(oid)
  661. writer.end('defs')
  662. for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
  663. gc, master_transform, all_transforms, path_codes, offsets,
  664. offsetTrans, facecolors, edgecolors, linewidths, linestyles,
  665. antialiaseds, urls, offset_position):
  666. clipid = self._get_clip(gc0)
  667. url = gc0.get_url()
  668. if url is not None:
  669. writer.start('a', attrib={'xlink:href': url})
  670. if clipid is not None:
  671. writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})
  672. attrib = {
  673. 'xlink:href': '#%s' % path_id,
  674. 'x': short_float_fmt(xo),
  675. 'y': short_float_fmt(self.height - yo),
  676. 'style': self._get_style(gc0, rgbFace)
  677. }
  678. writer.element('use', attrib=attrib)
  679. if clipid is not None:
  680. writer.end('g')
  681. if url is not None:
  682. writer.end('a')
  683. self._path_collection_id += 1
  684. def draw_gouraud_triangle(self, gc, points, colors, trans):
  685. # docstring inherited
  686. # This uses a method described here:
  687. #
  688. # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
  689. #
  690. # that uses three overlapping linear gradients to simulate a
  691. # Gouraud triangle. Each gradient goes from fully opaque in
  692. # one corner to fully transparent along the opposite edge.
  693. # The line between the stop points is perpendicular to the
  694. # opposite edge. Underlying these three gradients is a solid
  695. # triangle whose color is the average of all three points.
  696. writer = self.writer
  697. if not self._has_gouraud:
  698. self._has_gouraud = True
  699. writer.start(
  700. 'filter',
  701. id='colorAdd')
  702. writer.element(
  703. 'feComposite',
  704. attrib={'in': 'SourceGraphic'},
  705. in2='BackgroundImage',
  706. operator='arithmetic',
  707. k2="1", k3="1")
  708. writer.end('filter')
  709. # feColorMatrix filter to correct opacity
  710. writer.start(
  711. 'filter',
  712. id='colorMat')
  713. writer.element(
  714. 'feColorMatrix',
  715. attrib={'type': 'matrix'},
  716. values='1 0 0 0 0 \n0 1 0 0 0 \n0 0 1 0 0' +
  717. ' \n1 1 1 1 0 \n0 0 0 0 1 ')
  718. writer.end('filter')
  719. avg_color = np.average(colors, axis=0)
  720. if avg_color[-1] == 0:
  721. # Skip fully-transparent triangles
  722. return
  723. trans_and_flip = self._make_flip_transform(trans)
  724. tpoints = trans_and_flip.transform(points)
  725. writer.start('defs')
  726. for i in range(3):
  727. x1, y1 = tpoints[i]
  728. x2, y2 = tpoints[(i + 1) % 3]
  729. x3, y3 = tpoints[(i + 2) % 3]
  730. rgba_color = colors[i]
  731. if x2 == x3:
  732. xb = x2
  733. yb = y1
  734. elif y2 == y3:
  735. xb = x1
  736. yb = y2
  737. else:
  738. m1 = (y2 - y3) / (x2 - x3)
  739. b1 = y2 - (m1 * x2)
  740. m2 = -(1.0 / m1)
  741. b2 = y1 - (m2 * x1)
  742. xb = (-b1 + b2) / (m1 - m2)
  743. yb = m2 * xb + b2
  744. writer.start(
  745. 'linearGradient',
  746. id="GR%x_%d" % (self._n_gradients, i),
  747. gradientUnits="userSpaceOnUse",
  748. x1=short_float_fmt(x1), y1=short_float_fmt(y1),
  749. x2=short_float_fmt(xb), y2=short_float_fmt(yb))
  750. writer.element(
  751. 'stop',
  752. offset='1',
  753. style=generate_css({
  754. 'stop-color': rgb2hex(avg_color),
  755. 'stop-opacity': short_float_fmt(rgba_color[-1])}))
  756. writer.element(
  757. 'stop',
  758. offset='0',
  759. style=generate_css({'stop-color': rgb2hex(rgba_color),
  760. 'stop-opacity': "0"}))
  761. writer.end('linearGradient')
  762. writer.end('defs')
  763. # triangle formation using "path"
  764. dpath = "M " + short_float_fmt(x1)+',' + short_float_fmt(y1)
  765. dpath += " L " + short_float_fmt(x2) + ',' + short_float_fmt(y2)
  766. dpath += " " + short_float_fmt(x3) + ',' + short_float_fmt(y3) + " Z"
  767. writer.element(
  768. 'path',
  769. attrib={'d': dpath,
  770. 'fill': rgb2hex(avg_color),
  771. 'fill-opacity': '1',
  772. 'shape-rendering': "crispEdges"})
  773. writer.start(
  774. 'g',
  775. attrib={'stroke': "none",
  776. 'stroke-width': "0",
  777. 'shape-rendering': "crispEdges",
  778. 'filter': "url(#colorMat)"})
  779. writer.element(
  780. 'path',
  781. attrib={'d': dpath,
  782. 'fill': 'url(#GR%x_0)' % self._n_gradients,
  783. 'shape-rendering': "crispEdges"})
  784. writer.element(
  785. 'path',
  786. attrib={'d': dpath,
  787. 'fill': 'url(#GR%x_1)' % self._n_gradients,
  788. 'filter': 'url(#colorAdd)',
  789. 'shape-rendering': "crispEdges"})
  790. writer.element(
  791. 'path',
  792. attrib={'d': dpath,
  793. 'fill': 'url(#GR%x_2)' % self._n_gradients,
  794. 'filter': 'url(#colorAdd)',
  795. 'shape-rendering': "crispEdges"})
  796. writer.end('g')
  797. self._n_gradients += 1
  798. def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
  799. transform):
  800. attrib = {}
  801. clipid = self._get_clip(gc)
  802. if clipid is not None:
  803. attrib['clip-path'] = 'url(#%s)' % clipid
  804. self.writer.start('g', attrib=attrib)
  805. transform = transform.frozen()
  806. for tri, col in zip(triangles_array, colors_array):
  807. self.draw_gouraud_triangle(gc, tri, col, transform)
  808. self.writer.end('g')
  809. def option_scale_image(self):
  810. # docstring inherited
  811. return True
  812. def get_image_magnification(self):
  813. return self.image_dpi / 72.0
  814. def draw_image(self, gc, x, y, im, transform=None):
  815. # docstring inherited
  816. h, w = im.shape[:2]
  817. if w == 0 or h == 0:
  818. return
  819. attrib = {}
  820. clipid = self._get_clip(gc)
  821. if clipid is not None:
  822. # Can't apply clip-path directly to the image because the
  823. # image has a transformation, which would also be applied
  824. # to the clip-path
  825. self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid})
  826. oid = gc.get_gid()
  827. url = gc.get_url()
  828. if url is not None:
  829. self.writer.start('a', attrib={'xlink:href': url})
  830. if mpl.rcParams['svg.image_inline']:
  831. buf = BytesIO()
  832. Image.fromarray(im).save(buf, format="png")
  833. oid = oid or self._make_id('image', buf.getvalue())
  834. attrib['xlink:href'] = (
  835. "data:image/png;base64,\n" +
  836. base64.b64encode(buf.getvalue()).decode('ascii'))
  837. else:
  838. if self.basename is None:
  839. raise ValueError("Cannot save image data to filesystem when "
  840. "writing SVG to an in-memory buffer")
  841. filename = '{}.image{}.png'.format(
  842. self.basename, next(self._image_counter))
  843. _log.info('Writing image file for inclusion: %s', filename)
  844. Image.fromarray(im).save(filename)
  845. oid = oid or 'Im_' + self._make_id('image', filename)
  846. attrib['xlink:href'] = filename
  847. attrib['id'] = oid
  848. if transform is None:
  849. w = 72.0 * w / self.image_dpi
  850. h = 72.0 * h / self.image_dpi
  851. self.writer.element(
  852. 'image',
  853. transform=generate_transform([
  854. ('scale', (1, -1)), ('translate', (0, -h))]),
  855. x=short_float_fmt(x),
  856. y=short_float_fmt(-(self.height - y - h)),
  857. width=short_float_fmt(w), height=short_float_fmt(h),
  858. attrib=attrib)
  859. else:
  860. alpha = gc.get_alpha()
  861. if alpha != 1.0:
  862. attrib['opacity'] = short_float_fmt(alpha)
  863. flipped = (
  864. Affine2D().scale(1.0 / w, 1.0 / h) +
  865. transform +
  866. Affine2D()
  867. .translate(x, y)
  868. .scale(1.0, -1.0)
  869. .translate(0.0, self.height))
  870. attrib['transform'] = generate_transform(
  871. [('matrix', flipped.frozen())])
  872. attrib['style'] = (
  873. 'image-rendering:crisp-edges;'
  874. 'image-rendering:pixelated')
  875. self.writer.element(
  876. 'image',
  877. width=short_float_fmt(w), height=short_float_fmt(h),
  878. attrib=attrib)
  879. if url is not None:
  880. self.writer.end('a')
  881. if clipid is not None:
  882. self.writer.end('g')
  883. def _update_glyph_map_defs(self, glyph_map_new):
  884. """
  885. Emit definitions for not-yet-defined glyphs, and record them as having
  886. been defined.
  887. """
  888. writer = self.writer
  889. if glyph_map_new:
  890. writer.start('defs')
  891. for char_id, (vertices, codes) in glyph_map_new.items():
  892. char_id = self._adjust_char_id(char_id)
  893. path_data = self._convert_path(
  894. Path(vertices, codes), simplify=False)
  895. writer.element('path', id=char_id, d=path_data)
  896. writer.end('defs')
  897. self._glyph_map.update(glyph_map_new)
  898. def _adjust_char_id(self, char_id):
  899. return char_id.replace("%20", "_")
  900. def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
  901. """
  902. Draw the text by converting them to paths using the textpath module.
  903. Parameters
  904. ----------
  905. s : str
  906. text to be converted
  907. prop : `matplotlib.font_manager.FontProperties`
  908. font property
  909. ismath : bool
  910. If True, use mathtext parser. If "TeX", use *usetex* mode.
  911. """
  912. writer = self.writer
  913. writer.comment(s)
  914. glyph_map = self._glyph_map
  915. text2path = self._text2path
  916. color = rgb2hex(gc.get_rgb())
  917. fontsize = prop.get_size_in_points()
  918. style = {}
  919. if color != '#000000':
  920. style['fill'] = color
  921. alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
  922. if alpha != 1:
  923. style['opacity'] = short_float_fmt(alpha)
  924. font_scale = fontsize / text2path.FONT_SCALE
  925. attrib = {
  926. 'style': generate_css(style),
  927. 'transform': generate_transform([
  928. ('translate', (x, y)),
  929. ('rotate', (-angle,)),
  930. ('scale', (font_scale, -font_scale))]),
  931. }
  932. writer.start('g', attrib=attrib)
  933. if not ismath:
  934. font = text2path._get_font(prop)
  935. _glyphs = text2path.get_glyphs_with_font(
  936. font, s, glyph_map=glyph_map, return_new_glyphs_only=True)
  937. glyph_info, glyph_map_new, rects = _glyphs
  938. self._update_glyph_map_defs(glyph_map_new)
  939. for glyph_id, xposition, yposition, scale in glyph_info:
  940. attrib = {'xlink:href': '#%s' % glyph_id}
  941. if xposition != 0.0:
  942. attrib['x'] = short_float_fmt(xposition)
  943. if yposition != 0.0:
  944. attrib['y'] = short_float_fmt(yposition)
  945. writer.element('use', attrib=attrib)
  946. else:
  947. if ismath == "TeX":
  948. _glyphs = text2path.get_glyphs_tex(
  949. prop, s, glyph_map=glyph_map, return_new_glyphs_only=True)
  950. else:
  951. _glyphs = text2path.get_glyphs_mathtext(
  952. prop, s, glyph_map=glyph_map, return_new_glyphs_only=True)
  953. glyph_info, glyph_map_new, rects = _glyphs
  954. self._update_glyph_map_defs(glyph_map_new)
  955. for char_id, xposition, yposition, scale in glyph_info:
  956. char_id = self._adjust_char_id(char_id)
  957. writer.element(
  958. 'use',
  959. transform=generate_transform([
  960. ('translate', (xposition, yposition)),
  961. ('scale', (scale,)),
  962. ]),
  963. attrib={'xlink:href': '#%s' % char_id})
  964. for verts, codes in rects:
  965. path = Path(verts, codes)
  966. path_data = self._convert_path(path, simplify=False)
  967. writer.element('path', d=path_data)
  968. writer.end('g')
  969. def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
  970. writer = self.writer
  971. color = rgb2hex(gc.get_rgb())
  972. style = {}
  973. if color != '#000000':
  974. style['fill'] = color
  975. alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3]
  976. if alpha != 1:
  977. style['opacity'] = short_float_fmt(alpha)
  978. if not ismath:
  979. font = self._get_font(prop)
  980. font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
  981. attrib = {}
  982. style['font-family'] = str(font.family_name)
  983. style['font-weight'] = str(prop.get_weight()).lower()
  984. style['font-stretch'] = str(prop.get_stretch()).lower()
  985. style['font-style'] = prop.get_style().lower()
  986. # Must add "px" to workaround a Firefox bug
  987. style['font-size'] = short_float_fmt(prop.get_size()) + 'px'
  988. attrib['style'] = generate_css(style)
  989. if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"):
  990. # If text anchoring can be supported, get the original
  991. # coordinates and add alignment information.
  992. # Get anchor coordinates.
  993. transform = mtext.get_transform()
  994. ax, ay = transform.transform(mtext.get_unitless_position())
  995. ay = self.height - ay
  996. # Don't do vertical anchor alignment. Most applications do not
  997. # support 'alignment-baseline' yet. Apply the vertical layout
  998. # to the anchor point manually for now.
  999. angle_rad = np.deg2rad(angle)
  1000. dir_vert = np.array([np.sin(angle_rad), np.cos(angle_rad)])
  1001. v_offset = np.dot(dir_vert, [(x - ax), (y - ay)])
  1002. ax = ax + v_offset * dir_vert[0]
  1003. ay = ay + v_offset * dir_vert[1]
  1004. ha_mpl_to_svg = {'left': 'start', 'right': 'end',
  1005. 'center': 'middle'}
  1006. style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()]
  1007. attrib['x'] = short_float_fmt(ax)
  1008. attrib['y'] = short_float_fmt(ay)
  1009. attrib['style'] = generate_css(style)
  1010. attrib['transform'] = "rotate(%s, %s, %s)" % (
  1011. short_float_fmt(-angle),
  1012. short_float_fmt(ax),
  1013. short_float_fmt(ay))
  1014. writer.element('text', s, attrib=attrib)
  1015. else:
  1016. attrib['transform'] = generate_transform([
  1017. ('translate', (x, y)),
  1018. ('rotate', (-angle,))])
  1019. writer.element('text', s, attrib=attrib)
  1020. else:
  1021. writer.comment(s)
  1022. width, height, descent, svg_elements, used_characters = \
  1023. self.mathtext_parser.parse(s, 72, prop)
  1024. svg_glyphs = svg_elements.svg_glyphs
  1025. svg_rects = svg_elements.svg_rects
  1026. attrib = {}
  1027. attrib['style'] = generate_css(style)
  1028. attrib['transform'] = generate_transform([
  1029. ('translate', (x, y)),
  1030. ('rotate', (-angle,))])
  1031. # Apply attributes to 'g', not 'text', because we likely have some
  1032. # rectangles as well with the same style and transformation.
  1033. writer.start('g', attrib=attrib)
  1034. writer.start('text')
  1035. # Sort the characters by font, and output one tspan for each.
  1036. spans = OrderedDict()
  1037. for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
  1038. style = generate_css({
  1039. 'font-size': short_float_fmt(fontsize) + 'px',
  1040. 'font-family': font.family_name,
  1041. 'font-style': font.style_name.lower(),
  1042. 'font-weight': font.style_name.lower()})
  1043. if thetext == 32:
  1044. thetext = 0xa0 # non-breaking space
  1045. spans.setdefault(style, []).append((new_x, -new_y, thetext))
  1046. for style, chars in spans.items():
  1047. chars.sort()
  1048. if len({y for x, y, t in chars}) == 1: # Are all y's the same?
  1049. ys = str(chars[0][1])
  1050. else:
  1051. ys = ' '.join(str(c[1]) for c in chars)
  1052. attrib = {
  1053. 'style': style,
  1054. 'x': ' '.join(short_float_fmt(c[0]) for c in chars),
  1055. 'y': ys
  1056. }
  1057. writer.element(
  1058. 'tspan',
  1059. ''.join(chr(c[2]) for c in chars),
  1060. attrib=attrib)
  1061. writer.end('text')
  1062. if len(svg_rects):
  1063. for x, y, width, height in svg_rects:
  1064. writer.element(
  1065. 'rect',
  1066. x=short_float_fmt(x),
  1067. y=short_float_fmt(-y + height),
  1068. width=short_float_fmt(width),
  1069. height=short_float_fmt(height)
  1070. )
  1071. writer.end('g')
  1072. @cbook._delete_parameter("3.3", "ismath")
  1073. def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
  1074. # docstring inherited
  1075. self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")
  1076. def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
  1077. # docstring inherited
  1078. clipid = self._get_clip(gc)
  1079. if clipid is not None:
  1080. # Cannot apply clip-path directly to the text, because
  1081. # is has a transformation
  1082. self.writer.start(
  1083. 'g', attrib={'clip-path': 'url(#%s)' % clipid})
  1084. if gc.get_url() is not None:
  1085. self.writer.start('a', {'xlink:href': gc.get_url()})
  1086. if mpl.rcParams['svg.fonttype'] == 'path':
  1087. self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext)
  1088. else:
  1089. self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext)
  1090. if gc.get_url() is not None:
  1091. self.writer.end('a')
  1092. if clipid is not None:
  1093. self.writer.end('g')
  1094. def flipy(self):
  1095. # docstring inherited
  1096. return True
  1097. def get_canvas_width_height(self):
  1098. # docstring inherited
  1099. return self.width, self.height
  1100. def get_text_width_height_descent(self, s, prop, ismath):
  1101. # docstring inherited
  1102. return self._text2path.get_text_width_height_descent(s, prop, ismath)
  1103. class FigureCanvasSVG(FigureCanvasBase):
  1104. filetypes = {'svg': 'Scalable Vector Graphics',
  1105. 'svgz': 'Scalable Vector Graphics'}
  1106. fixed_dpi = 72
  1107. def print_svg(self, filename, *args, **kwargs):
  1108. """
  1109. Parameters
  1110. ----------
  1111. filename : str or path-like or file-like
  1112. Output target; if a string, a file will be opened for writing.
  1113. metadata : Dict[str, Any], optional
  1114. Metadata in the SVG file defined as key-value pairs of strings,
  1115. datetimes, or lists of strings, e.g., ``{'Creator': 'My software',
  1116. 'Contributor': ['Me', 'My Friend'], 'Title': 'Awesome'}``.
  1117. The standard keys and their value types are:
  1118. * *str*: ``'Coverage'``, ``'Description'``, ``'Format'``,
  1119. ``'Identifier'``, ``'Language'``, ``'Relation'``, ``'Source'``,
  1120. ``'Title'``, and ``'Type'``.
  1121. * *str* or *list of str*: ``'Contributor'``, ``'Creator'``,
  1122. ``'Keywords'``, ``'Publisher'``, and ``'Rights'``.
  1123. * *str*, *date*, *datetime*, or *tuple* of same: ``'Date'``. If a
  1124. non-*str*, then it will be formatted as ISO 8601.
  1125. Values have been predefined for ``'Creator'``, ``'Date'``,
  1126. ``'Format'``, and ``'Type'``. They can be removed by setting them
  1127. to `None`.
  1128. Information is encoded as `Dublin Core Metadata`__.
  1129. .. _DC: https://www.dublincore.org/specifications/dublin-core/
  1130. __ DC_
  1131. """
  1132. with cbook.open_file_cm(filename, "w", encoding="utf-8") as fh:
  1133. filename = getattr(fh, 'name', '')
  1134. if not isinstance(filename, str):
  1135. filename = ''
  1136. if cbook.file_requires_unicode(fh):
  1137. detach = False
  1138. else:
  1139. fh = TextIOWrapper(fh, 'utf-8')
  1140. detach = True
  1141. self._print_svg(filename, fh, **kwargs)
  1142. # Detach underlying stream from wrapper so that it remains open in
  1143. # the caller.
  1144. if detach:
  1145. fh.detach()
  1146. def print_svgz(self, filename, *args, **kwargs):
  1147. with cbook.open_file_cm(filename, "wb") as fh, \
  1148. gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter:
  1149. return self.print_svg(gzipwriter)
  1150. @_check_savefig_extra_args
  1151. def _print_svg(self, filename, fh, *, dpi=72, bbox_inches_restore=None,
  1152. metadata=None):
  1153. self.figure.set_dpi(72.0)
  1154. width, height = self.figure.get_size_inches()
  1155. w, h = width * 72, height * 72
  1156. renderer = MixedModeRenderer(
  1157. self.figure, width, height, dpi,
  1158. RendererSVG(w, h, fh, filename, dpi, metadata=metadata),
  1159. bbox_inches_restore=bbox_inches_restore)
  1160. self.figure.draw(renderer)
  1161. renderer.finalize()
  1162. def get_default_filetype(self):
  1163. return 'svg'
  1164. FigureManagerSVG = FigureManagerBase
  1165. svgProlog = """\
  1166. <?xml version="1.0" encoding="utf-8" standalone="no"?>
  1167. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  1168. "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
  1169. <!-- Created with matplotlib (https://matplotlib.org/) -->
  1170. """
  1171. @_Backend.export
  1172. class _BackendSVG(_Backend):
  1173. FigureCanvas = FigureCanvasSVG