From 74ab8b646388693e0a8af7628145821bb53d8076 Mon Sep 17 00:00:00 2001 From: DavidAG Date: Wed, 17 Dec 2025 18:27:07 +0100 Subject: [PATCH 1/3] Optimize fill_between SVG/PDF size by enabling path simplification --- lib/matplotlib/backends/backend_pdf.py | 1574 ++++++++++++++---------- lib/matplotlib/backends/backend_svg.py | 992 ++++++++------- lib/matplotlib/path.py | 326 ++--- lib/matplotlib/tests/test_path.py | 377 +++--- 4 files changed, 1888 insertions(+), 1381 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index d63808eb3925..f5c2b95bcbb9 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -29,8 +29,12 @@ from matplotlib import _api, _text_helpers, _type1font, cbook, dviread from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, - RendererBase) + _Backend, + FigureCanvasBase, + FigureManagerBase, + GraphicsContextBase, + RendererBase, +) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager @@ -107,11 +111,11 @@ def _fill(strings, linelen=75): if currpos + length < linelen: currpos += length + 1 else: - result.append(b' '.join(strings[lasti:i])) + result.append(b" ".join(strings[lasti:i])) lasti = i currpos = length - result.append(b' '.join(strings[lasti:])) - return b'\n'.join(result) + result.append(b" ".join(strings[lasti:])) + return b"\n".join(result) def _create_pdf_info_dict(backend, metadata): @@ -154,49 +158,56 @@ def _create_pdf_info_dict(backend, metadata): source_date = datetime.today() info = { - 'Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org', - 'Producer': f'Matplotlib {backend} backend v{mpl.__version__}', - 'CreationDate': source_date, - **metadata + "Creator": f"Matplotlib v{mpl.__version__}, https://matplotlib.org", + "Producer": f"Matplotlib {backend} backend v{mpl.__version__}", + "CreationDate": source_date, + **metadata, } info = {k: v for (k, v) in info.items() if v is not None} def is_string_like(x): return isinstance(x, str) + is_string_like.text_for_warning = "an instance of str" def is_date(x): return isinstance(x, datetime) + is_date.text_for_warning = "an instance of datetime.datetime" def check_trapped(x): if isinstance(x, Name): - return x.name in (b'True', b'False', b'Unknown') + return x.name in (b"True", b"False", b"Unknown") else: - return x in ('True', 'False', 'Unknown') + return x in ("True", "False", "Unknown") + check_trapped.text_for_warning = 'one of {"True", "False", "Unknown"}' keywords = { - 'Title': is_string_like, - 'Author': is_string_like, - 'Subject': is_string_like, - 'Keywords': is_string_like, - 'Creator': is_string_like, - 'Producer': is_string_like, - 'CreationDate': is_date, - 'ModDate': is_date, - 'Trapped': check_trapped, + "Title": is_string_like, + "Author": is_string_like, + "Subject": is_string_like, + "Keywords": is_string_like, + "Creator": is_string_like, + "Producer": is_string_like, + "CreationDate": is_date, + "ModDate": is_date, + "Trapped": check_trapped, } for k in info: if k not in keywords: - _api.warn_external(f'Unknown infodict keyword: {k!r}. ' - f'Must be one of {set(keywords)!r}.') + _api.warn_external( + f"Unknown infodict keyword: {k!r}. " + f"Must be one of {set(keywords)!r}." + ) elif not keywords[k](info[k]): - _api.warn_external(f'Bad value for infodict keyword {k}. ' - f'Got {info[k]!r} which is not ' - f'{keywords[k].text_for_warning}.') - if 'Trapped' in info: - info['Trapped'] = Name(info['Trapped']) + _api.warn_external( + f"Bad value for infodict keyword {k}. " + f"Got {info[k]!r} which is not " + f"{keywords[k].text_for_warning}." + ) + if "Trapped" in info: + info["Trapped"] = Name(info["Trapped"]) return info @@ -207,7 +218,7 @@ def _datetime_to_pdf(d): Used for PDF and PGF. """ - r = d.strftime('D:%Y%m%d%H%M%S') + r = d.strftime("D:%Y%m%d%H%M%S") z = d.utcoffset() if z is not None: z = z.seconds @@ -217,7 +228,7 @@ def _datetime_to_pdf(d): else: z = time.timezone if z == 0: - r += 'Z' + r += "Z" elif z < 0: r += "+%02d'%02d'" % ((-z) // 3600, (-z) % 3600) else: @@ -248,8 +259,7 @@ def _get_coordinates_of_block(x, y, width, height, angle=0): rotated rectangle. """ - vertices = _calculate_quad_point_coordinates(x, y, width, - height, angle) + vertices = _calculate_quad_point_coordinates(x, y, width, height, angle) # Find min and max values for rectangle # adjust so that QuadPoints is inside Rect @@ -262,8 +272,10 @@ def _get_coordinates_of_block(x, y, width, height, angle=0): min_y = min(v[1] for v in vertices) - pad max_x = max(v[0] for v in vertices) + pad max_y = max(v[1] for v in vertices) + pad - return (tuple(itertools.chain.from_iterable(vertices)), - (min_x, min_y, max_x, max_y)) + return ( + tuple(itertools.chain.from_iterable(vertices)), + (min_x, min_y, max_x, max_y), + ) def _get_link_annotation(gc, x, y, width, height, angle=0): @@ -272,18 +284,18 @@ def _get_link_annotation(gc, x, y, width, height, angle=0): """ quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle) link_annotation = { - 'Type': Name('Annot'), - 'Subtype': Name('Link'), - 'Rect': rect, - 'Border': [0, 0, 0], - 'A': { - 'S': Name('URI'), - 'URI': gc.get_url(), + "Type": Name("Annot"), + "Subtype": Name("Link"), + "Rect": rect, + "Border": [0, 0, 0], + "A": { + "S": Name("URI"), + "URI": gc.get_url(), }, } if angle % 90: # Add QuadPoints - link_annotation['QuadPoints'] = quadpoints + link_annotation["QuadPoints"] = quadpoints return link_annotation @@ -292,15 +304,16 @@ def _get_link_annotation(gc, x, y, width, height, angle=0): # However, sf bug #2708559 shows that the carriage return character may get # read as a newline; these characters correspond to \gamma and \Omega in TeX's # math font encoding. Escaping them fixes the bug. -_str_escapes = str.maketrans({ - '\\': '\\\\', '(': '\\(', ')': '\\)', '\n': '\\n', '\r': '\\r'}) +_str_escapes = str.maketrans( + {"\\": "\\\\", "(": "\\(", ")": "\\)", "\n": "\\n", "\r": "\\r"} +) def pdfRepr(obj): """Map Python objects to PDF syntax.""" # Some objects defined later have their own pdfRepr method. - if hasattr(obj, 'pdfRepr'): + if hasattr(obj, "pdfRepr"): return obj.pdfRepr() # Floats. PDF does not have exponential notation (1.0e-10) so we @@ -310,12 +323,12 @@ def pdfRepr(obj): if not np.isfinite(obj): raise ValueError("Can only output finite numbers in PDF") r = b"%.10f" % obj - return r.rstrip(b'0').rstrip(b'.') + return r.rstrip(b"0").rstrip(b".") # Booleans. Needs to be tested before integers since # isinstance(True, int) is true. elif isinstance(obj, bool): - return [b'false', b'true'][obj] + return [b"false", b"true"][obj] # Integers are written as such. elif isinstance(obj, (int, np.integer)): @@ -323,8 +336,11 @@ def pdfRepr(obj): # Non-ASCII Unicode strings are encoded in UTF-16BE with byte-order mark. elif isinstance(obj, str): - return pdfRepr(obj.encode('ascii') if obj.isascii() - else codecs.BOM_UTF16_BE + obj.encode('UTF-16BE')) + return pdfRepr( + obj.encode("ascii") + if obj.isascii() + else codecs.BOM_UTF16_BE + obj.encode("UTF-16BE") + ) # Strings are written in parentheses, with backslashes and parens # escaped. Actually balanced parens are allowed, but it is @@ -333,20 +349,23 @@ def pdfRepr(obj): # Despite the extra decode/encode, translate is faster than regex. elif isinstance(obj, bytes): return ( - b'(' + - obj.decode('latin-1').translate(_str_escapes).encode('latin-1') - + b')') + b"(" + + obj.decode("latin-1").translate(_str_escapes).encode("latin-1") + + b")" + ) # Dictionaries. The keys must be PDF names, so if we find strings # there, we make Name objects from them. The values may be # anything, so the caller must ensure that PDF names are # represented as Name objects. elif isinstance(obj, dict): - return _fill([ - b"<<", - *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], - b">>", - ]) + return _fill( + [ + b"<<", + *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], + b">>", + ] + ) # Lists. elif isinstance(obj, (list, tuple)): @@ -354,7 +373,7 @@ def pdfRepr(obj): # The null keyword. elif obj is None: - return b'null' + return b"null" # A date. elif isinstance(obj, datetime): @@ -365,8 +384,7 @@ def pdfRepr(obj): return _fill([pdfRepr(val) for val in obj.bounds]) else: - raise TypeError(f"Don't know a PDF representation for {type(obj)} " - "objects") + raise TypeError(f"Don't know a PDF representation for {type(obj)} " "objects") def _font_supports_glyph(fonttype, glyph): @@ -410,23 +428,23 @@ def write(self, contents, file): @total_ordering class Name: """PDF name object.""" - __slots__ = ('name',) - _hexify = {c: '#%02x' % c - for c in {*range(256)} - {*range(ord('!'), ord('~') + 1)}} + + __slots__ = ("name",) + _hexify = {c: "#%02x" % c for c in {*range(256)} - {*range(ord("!"), ord("~") + 1)}} def __init__(self, name): if isinstance(name, Name): self.name = name.name else: if isinstance(name, bytes): - name = name.decode('ascii') - self.name = name.translate(self._hexify).encode('ascii') + name = name.decode("ascii") + self.name = name.translate(self._hexify).encode("ascii") def __repr__(self): return "" % self.name def __str__(self): - return '/' + self.name.decode('ascii') + return "/" + self.name.decode("ascii") def __eq__(self, other): return isinstance(other, Name) and self.name == other.name @@ -438,11 +456,12 @@ def __hash__(self): return hash(self.name) def pdfRepr(self): - return b'/' + self.name + return b"/" + self.name class Verbatim: """Store verbatim PDF command content for later inclusion in the stream.""" + def __init__(self, x): self._x = x @@ -453,43 +472,43 @@ def pdfRepr(self): class Op(Enum): """PDF operators (not an exhaustive list).""" - close_fill_stroke = b'b' - fill_stroke = b'B' - fill = b'f' - closepath = b'h' - close_stroke = b's' - stroke = b'S' - endpath = b'n' - begin_text = b'BT' - end_text = b'ET' - curveto = b'c' - rectangle = b're' - lineto = b'l' - moveto = b'm' - concat_matrix = b'cm' - use_xobject = b'Do' - setgray_stroke = b'G' - setgray_nonstroke = b'g' - setrgb_stroke = b'RG' - setrgb_nonstroke = b'rg' - setcolorspace_stroke = b'CS' - setcolorspace_nonstroke = b'cs' - setcolor_stroke = b'SCN' - setcolor_nonstroke = b'scn' - setdash = b'd' - setlinejoin = b'j' - setlinecap = b'J' - setgstate = b'gs' - gsave = b'q' - grestore = b'Q' - textpos = b'Td' - selectfont = b'Tf' - textmatrix = b'Tm' - show = b'Tj' - showkern = b'TJ' - setlinewidth = b'w' - clip = b'W' - shading = b'sh' + close_fill_stroke = b"b" + fill_stroke = b"B" + fill = b"f" + closepath = b"h" + close_stroke = b"s" + stroke = b"S" + endpath = b"n" + begin_text = b"BT" + end_text = b"ET" + curveto = b"c" + rectangle = b"re" + lineto = b"l" + moveto = b"m" + concat_matrix = b"cm" + use_xobject = b"Do" + setgray_stroke = b"G" + setgray_nonstroke = b"g" + setrgb_stroke = b"RG" + setrgb_nonstroke = b"rg" + setcolorspace_stroke = b"CS" + setcolorspace_nonstroke = b"cs" + setcolor_stroke = b"SCN" + setcolor_nonstroke = b"scn" + setdash = b"d" + setlinejoin = b"j" + setlinecap = b"J" + setgstate = b"gs" + gsave = b"q" + grestore = b"Q" + textpos = b"Td" + selectfont = b"Tf" + textmatrix = b"Tm" + show = b"Tj" + showkern = b"TJ" + setlinewidth = b"w" + clip = b"W" + shading = b"sh" def pdfRepr(self): return self.value @@ -525,7 +544,8 @@ class Stream: This has no pdfRepr method. Instead, call begin(), then output the contents of the stream by calling write(), and finally call end(). """ - __slots__ = ('id', 'len', 'pdfFile', 'file', 'compressobj', 'extra', 'pos') + + __slots__ = ("id", "len", "pdfFile", "file", "compressobj", "extra", "pos") def __init__(self, id, len, file, extra=None, png=None): """ @@ -543,23 +563,21 @@ def __init__(self, id, len, file, extra=None, png=None): png : dict or None If the data is already png encoded, the decode parameters. """ - self.id = id # object id - self.len = len # id of length object + self.id = id # object id + self.len = len # id of length object self.pdfFile = file - self.file = file.fh # file to which the stream is written + self.file = file.fh # file to which the stream is written self.compressobj = None # compression object if extra is None: self.extra = dict() else: self.extra = extra.copy() if png is not None: - self.extra.update({'Filter': Name('FlateDecode'), - 'DecodeParms': png}) + self.extra.update({"Filter": Name("FlateDecode"), "DecodeParms": png}) self.pdfFile.recordXref(self.id) - if mpl.rcParams['pdf.compression'] and not png: - self.compressobj = zlib.compressobj( - mpl.rcParams['pdf.compression']) + if mpl.rcParams["pdf.compression"] and not png: + self.compressobj = zlib.compressobj(mpl.rcParams["pdf.compression"]) if self.len is None: self.file = BytesIO() else: @@ -570,9 +588,9 @@ def _writeHeader(self): write = self.file.write write(b"%d 0 obj\n" % self.id) dict = self.extra - dict['Length'] = self.len - if mpl.rcParams['pdf.compression']: - dict['Filter'] = Name('FlateDecode') + dict["Length"] = self.len + if mpl.rcParams["pdf.compression"]: + dict["Filter"] = Name("FlateDecode") write(pdfRepr(dict)) write(b"\nstream\n") @@ -620,7 +638,7 @@ def _get_pdf_charprocs(font_path, glyph_ids): # NOTE: We should be using round(), but instead use # "(x+.5).astype(int)" to keep backcompat with the old ttconv code # (this is different for negative x's). - d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int) + d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + 0.5).astype(int) v, c = font.get_path() v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's). # Backcompat with old ttconv code: control points between two quads are @@ -631,29 +649,42 @@ def _get_pdf_charprocs(font_path, glyph_ids): # re-interpolating them. Note that occasionally (e.g. with DejaVu Sans # glyph "0") a point detected as "implicit" is actually explicit, and # will thus be shifted by 1. - quads, = np.nonzero(c == 3) + (quads,) = np.nonzero(c == 3) quads_on = quads[1::2] quads_mid_on = np.array( - sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int) + sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int + ) implicit = quads_mid_on[ - (v[quads_mid_on] # As above, use astype(int), not // division - == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int)) - .all(axis=1)] + ( + v[quads_mid_on] # As above, use astype(int), not // division + == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int) + ).all(axis=1) + ] if (font.postscript_name, glyph_id) in [ - ("DejaVuSerif-Italic", 77), # j - ("DejaVuSerif-Italic", 135), # \AA + ("DejaVuSerif-Italic", 77), # j + ("DejaVuSerif-Italic", 135), # \AA ]: v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1). - v = (v * conv + .5).astype(int) # As above re: truncation vs rounding. - v[implicit] = (( # Fix implicit points; again, truncate. - (v[implicit - 1] + v[implicit + 1]) / 2).astype(int)) + v = (v * conv + 0.5).astype(int) # As above re: truncation vs rounding. + v[implicit] = ( # Fix implicit points; again, truncate. + (v[implicit - 1] + v[implicit + 1]) / 2 + ).astype(int) procs[font.get_glyph_name(glyph_id)] = ( - " ".join(map(str, d1)).encode("ascii") + b" d1\n" + " ".join(map(str, d1)).encode("ascii") + + b" d1\n" + _path.convert_to_string( - Path(v, c), None, None, False, None, -1, + Path(v, c), + None, + None, + False, + None, + -1, # no code for quad Beziers triggers auto-conversion to cubics. - [b"m", b"l", b"", b"c", b"h"], True) - + b"f") + [b"m", b"l", b"", b"c", b"h"], + True, + ) + + b"f" + ) return procs @@ -680,7 +711,7 @@ def __init__(self, filename, metadata=None): super().__init__() self._object_seq = itertools.count(1) # consumed by reserveObject - self.xrefTable = [[0, 65535, 'the zero object']] + self.xrefTable = [[0, 65535, "the zero object"]] self.passed_in_file_object = False self.original_file_like = None self.tell_base = 0 @@ -697,44 +728,43 @@ def __init__(self, filename, metadata=None): self.fh = fh self.currentstream = None # stream object to write to, if any - fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha + fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha # Output some eight-bit chars as a comment so various utilities # recognize the file as binary by looking at the first few # lines (see note in section 3.4.1 of the PDF reference). fh.write(b"%\254\334 \253\272\n") - self.rootObject = self.reserveObject('root') - self.pagesObject = self.reserveObject('pages') + self.rootObject = self.reserveObject("root") + self.pagesObject = self.reserveObject("pages") self.pageList = [] - self.fontObject = self.reserveObject('fonts') - self._extGStateObject = self.reserveObject('extended graphics states') - self.hatchObject = self.reserveObject('tiling patterns') - self.gouraudObject = self.reserveObject('Gouraud triangles') - self.XObjectObject = self.reserveObject('external objects') - self.resourceObject = self.reserveObject('resources') - - root = {'Type': Name('Catalog'), - 'Pages': self.pagesObject} + self.fontObject = self.reserveObject("fonts") + self._extGStateObject = self.reserveObject("extended graphics states") + self.hatchObject = self.reserveObject("tiling patterns") + self.gouraudObject = self.reserveObject("Gouraud triangles") + self.XObjectObject = self.reserveObject("external objects") + self.resourceObject = self.reserveObject("resources") + + root = {"Type": Name("Catalog"), "Pages": self.pagesObject} self.writeObject(self.rootObject, root) - self.infoDict = _create_pdf_info_dict('pdf', metadata or {}) + self.infoDict = _create_pdf_info_dict("pdf", metadata or {}) - self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1)) - self._fontNames = {} # maps filenames to internal font names - self._dviFontInfo = {} # maps pdf names to dvifonts + self._internal_font_seq = (Name(f"F{i}") for i in itertools.count(1)) + self._fontNames = {} # maps filenames to internal font names + self._dviFontInfo = {} # maps pdf names to dvifonts self._character_tracker = _backend_pdf_ps.CharacterTracker() - self.alphaStates = {} # maps alpha values to graphics state objects - self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) + self.alphaStates = {} # maps alpha values to graphics state objects + self._alpha_state_seq = (Name(f"A{i}") for i in itertools.count(1)) self._soft_mask_states = {} - self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) + self._soft_mask_seq = (Name(f"SM{i}") for i in itertools.count(1)) self._soft_mask_groups = [] self._hatch_patterns = {} - self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) + self._hatch_pattern_seq = (Name(f"H{i}") for i in itertools.count(1)) self.gouraudTriangles = [] self._images = {} - self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) + self._image_seq = (Name(f"I{i}") for i in itertools.count(1)) self.markers = {} self.multi_byte_charprocs = {} @@ -755,12 +785,14 @@ def __init__(self, filename, metadata=None): # Write resource dictionary. # Possibly TODO: more general ExtGState (graphics state dictionaries) # ColorSpace Pattern Shading Properties - resources = {'Font': self.fontObject, - 'XObject': self.XObjectObject, - 'ExtGState': self._extGStateObject, - 'Pattern': self.hatchObject, - 'Shading': self.gouraudObject, - 'ProcSet': procsets} + resources = { + "Font": self.fontObject, + "XObject": self.XObjectObject, + "ExtGState": self._extGStateObject, + "Pattern": self.hatchObject, + "Shading": self.gouraudObject, + "ProcSet": procsets, + } self.writeObject(self.resourceObject, resources) fontNames = _api.deprecated("3.11")(property(lambda self: self._fontNames)) @@ -770,14 +802,16 @@ def __init__(self, filename, metadata=None): @property def dviFontInfo(self): d = {} - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap(dviread.find_tex_file("pdftex.map")) for pdfname, dvifont in self._dviFontInfo.items(): psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( "No usable font file found for {} (TeX: {}); " - "the font may lack a Type-1 version" - .format(psfont.psname, dvifont.texname)) + "the font may lack a Type-1 version".format( + psfont.psname, dvifont.texname + ) + ) d[dvifont.texname] = types.SimpleNamespace( dvifont=dvifont, pdfname=pdfname, @@ -792,38 +826,41 @@ def newPage(self, width, height): self.endStream() self.width, self.height = width, height - contentObject = self.reserveObject('page contents') - annotsObject = self.reserveObject('annotations') - thePage = {'Type': Name('Page'), - 'Parent': self.pagesObject, - 'Resources': self.resourceObject, - 'MediaBox': [0, 0, 72 * width, 72 * height], - 'Contents': contentObject, - 'Annots': annotsObject, - } - pageObject = self.reserveObject('page') + contentObject = self.reserveObject("page contents") + annotsObject = self.reserveObject("annotations") + thePage = { + "Type": Name("Page"), + "Parent": self.pagesObject, + "Resources": self.resourceObject, + "MediaBox": [0, 0, 72 * width, 72 * height], + "Contents": contentObject, + "Annots": annotsObject, + } + pageObject = self.reserveObject("page") self.writeObject(pageObject, thePage) self.pageList.append(pageObject) self._annotations.append((annotsObject, self.pageAnnotations)) - self.beginStream(contentObject.id, - self.reserveObject('length of content stream')) + self.beginStream( + contentObject.id, self.reserveObject("length of content stream") + ) # Initialize the pdf graphics state to match the default Matplotlib # graphics context (colorspace and joinstyle). - self.output(Name('DeviceRGB'), Op.setcolorspace_stroke) - self.output(Name('DeviceRGB'), Op.setcolorspace_nonstroke) - self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin) + self.output(Name("DeviceRGB"), Op.setcolorspace_stroke) + self.output(Name("DeviceRGB"), Op.setcolorspace_nonstroke) + self.output(GraphicsContextPdf.joinstyles["round"], Op.setlinejoin) # Clear the list of annotations for the next page self.pageAnnotations = [] def newTextnote(self, text, positionRect=[-100, -100, 0, 0]): # Create a new annotation of type text - theNote = {'Type': Name('Annot'), - 'Subtype': Name('Text'), - 'Contents': text, - 'Rect': positionRect, - } + theNote = { + "Type": Name("Annot"), + "Subtype": Name("Text"), + "Contents": text, + "Rect": positionRect, + } self.pageAnnotations.append(theNote) @staticmethod @@ -834,13 +871,12 @@ def _get_subset_prefix(charset): The prefix is six uppercase letters followed by a plus sign; see PDF reference section 5.5.3 Font Subsets. """ + def toStr(n, base): if n < base: return string.ascii_uppercase[n] else: - return ( - toStr(n // base, base) + string.ascii_uppercase[n % base] - ) + return toStr(n // base, base) + string.ascii_uppercase[n % base] # encode to string using base 26 hashed = hash(charset) % ((sys.maxsize + 1) * 2) @@ -863,23 +899,21 @@ def finalize(self): self._write_soft_mask_groups() self.writeHatches() self.writeGouraudTriangles() - xobjects = { - name: ob for image, name, ob in self._images.values()} + xobjects = {name: ob for image, name, ob in self._images.values()} for tup in self.markers.values(): xobjects[tup[0]] = tup[1] for name, value in self.multi_byte_charprocs.items(): xobjects[name] = value - for name, path, trans, ob, join, cap, padding, filled, stroked \ - in self.paths: + for name, path, trans, ob, join, cap, padding, filled, stroked in self.paths: xobjects[name] = ob self.writeObject(self.XObjectObject, xobjects) self.writeImages() self.writeMarkers() self.writePathCollectionTemplates() - self.writeObject(self.pagesObject, - {'Type': Name('Pages'), - 'Kids': self.pageList, - 'Count': len(self.pageList)}) + self.writeObject( + self.pagesObject, + {"Type": Name("Pages"), "Kids": self.pageList, "Count": len(self.pageList)}, + ) self.writeInfoDict() # Finalize the file @@ -905,7 +939,7 @@ def write(self, data): def output(self, *data): self.write(_fill([pdfRepr(x) for x in data])) - self.write(b'\n') + self.write(b"\n") def beginStream(self, id, len, extra=None, png=None): assert self.currentstream is None @@ -934,9 +968,9 @@ def fontName(self, fontprop): if isinstance(fontprop, str): filenames = [fontprop] - elif mpl.rcParams['pdf.use14corefonts']: + elif mpl.rcParams["pdf.use14corefonts"]: filenames = _fontManager._find_fonts_by_props( - fontprop, fontext='afm', directory=RendererPdf._afm_font_dir + fontprop, fontext="afm", directory=RendererPdf._afm_font_dir ) else: filenames = _fontManager._find_fonts_by_props(fontprop) @@ -948,7 +982,7 @@ def fontName(self, fontprop): if Fx is None: Fx = next(self._internal_font_seq) self._fontNames[fname] = Fx - _log.debug('Assigning font %s = %r', Fx, fname) + _log.debug("Assigning font %s = %r", Fx, fname) if not first_Fx: first_Fx = Fx @@ -963,56 +997,60 @@ def dviFontName(self, dvifont): Register the font internally (in ``_dviFontInfo``) if not yet registered. """ pdfname = Name(f"F-{dvifont.texname.decode('ascii')}") - _log.debug('Assigning font %s = %s (dvi)', pdfname, dvifont.texname) + _log.debug("Assigning font %s = %s (dvi)", pdfname, dvifont.texname) self._dviFontInfo[pdfname] = dvifont return Name(pdfname) def writeFonts(self): fonts = {} for pdfname, dvifont in sorted(self._dviFontInfo.items()): - _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) + _log.debug("Embedding Type-1 font %s from dvi.", dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) for filename in sorted(self._fontNames): Fx = self._fontNames[filename] - _log.debug('Embedding font %s.', filename) - if filename.endswith('.afm'): + _log.debug("Embedding font %s.", filename) + if filename.endswith(".afm"): # from pdf.use14corefonts - _log.debug('Writing AFM font.') + _log.debug("Writing AFM font.") fonts[Fx] = self._write_afm_font(filename) else: # a normal TrueType font - _log.debug('Writing TrueType font.') + _log.debug("Writing TrueType font.") chars = self._character_tracker.used.get(filename) if chars: fonts[Fx] = self.embedTTF(filename, chars) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): - with open(filename, 'rb') as fh: + with open(filename, "rb") as fh: font = AFM(fh) fontname = font.get_fontname() - fontdict = {'Type': Name('Font'), - 'Subtype': Name('Type1'), - 'BaseFont': Name(fontname), - 'Encoding': Name('WinAnsiEncoding')} - fontdictObject = self.reserveObject('font dictionary') + fontdict = { + "Type": Name("Font"), + "Subtype": Name("Type1"), + "BaseFont": Name(fontname), + "Encoding": Name("WinAnsiEncoding"), + } + fontdictObject = self.reserveObject("font dictionary") self.writeObject(fontdictObject, fontdict) return fontdictObject def _embedTeXFont(self, dvifont): - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap(dviread.find_tex_file("pdftex.map")) psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( "No usable font file found for {} (TeX: {}); " - "the font may lack a Type-1 version" - .format(psfont.psname, dvifont.texname)) + "the font may lack a Type-1 version".format( + psfont.psname, dvifont.texname + ) + ) # The font dictionary is the top-level object describing a font - fontdictObject = self.reserveObject('font dictionary') + fontdictObject = self.reserveObject("font dictionary") fontdict = { - 'Type': Name('Font'), - 'Subtype': Name('Type1'), + "Type": Name("Font"), + "Subtype": Name("Type1"), } # Read the font file and apply any encoding changes and effects @@ -1028,19 +1066,24 @@ def _embedTeXFont(self, dvifont): # for that subset, and compute various properties based on the encoding. chars = frozenset(self._character_tracker.used[dvifont.fname]) t1font = t1font.subset(chars, self._get_subset_prefix(chars)) - fontdict['BaseFont'] = Name(t1font.prop['FontName']) + fontdict["BaseFont"] = Name(t1font.prop["FontName"]) # createType1Descriptor writes the font data as a side effect - fontdict['FontDescriptor'] = self.createType1Descriptor(t1font) - encoding = t1font.prop['Encoding'] - fontdict['Encoding'] = self._generate_encoding(encoding) - fc = fontdict['FirstChar'] = min(encoding.keys(), default=0) - lc = fontdict['LastChar'] = max(encoding.keys(), default=255) + fontdict["FontDescriptor"] = self.createType1Descriptor(t1font) + encoding = t1font.prop["Encoding"] + fontdict["Encoding"] = self._generate_encoding(encoding) + fc = fontdict["FirstChar"] = min(encoding.keys(), default=0) + lc = fontdict["LastChar"] = max(encoding.keys(), default=255) # Convert glyph widths from TeX 12.20 fixed point to 1/1000 text space units font_metrics = dvifont._metrics - widths = [(1000 * glyph_metrics.tex_width) >> 20 - if (glyph_metrics := font_metrics.get_metrics(char)) else 0 - for char in range(fc, lc + 1)] - fontdict['Widths'] = widthsObject = self.reserveObject('glyph widths') + widths = [ + ( + (1000 * glyph_metrics.tex_width) >> 20 + if (glyph_metrics := font_metrics.get_metrics(char)) + else 0 + ) + for char in range(fc, lc + 1) + ] + fontdict["Widths"] = widthsObject = self.reserveObject("glyph widths") self.writeObject(widthsObject, widths) self.writeObject(fontdictObject, fontdict) return fontdictObject @@ -1053,20 +1096,17 @@ def _generate_encoding(self, encoding): result.append(code) prev = code result.append(Name(name)) - return { - 'Type': Name('Encoding'), - 'Differences': result - } + return {"Type": Name("Encoding"), "Differences": result} @_api.delete_parameter("3.11", "fontfile") def createType1Descriptor(self, t1font, fontfile=None): # Create and write the font descriptor and the font file # of a Type-1 font - fontdescObject = self.reserveObject('font descriptor') - fontfileObject = self.reserveObject('font file') + fontdescObject = self.reserveObject("font descriptor") + fontfileObject = self.reserveObject("font file") - italic_angle = t1font.prop['ItalicAngle'] - fixed_pitch = t1font.prop['isFixedPitch'] + italic_angle = t1font.prop["ItalicAngle"] + fixed_pitch = t1font.prop["isFixedPitch"] flags = 0 # fixed width @@ -1094,47 +1134,50 @@ def createType1Descriptor(self, t1font, fontfile=None): if 0: flags |= 1 << 18 - encoding = t1font.prop['Encoding'] - charset = ''.join( - sorted( - f'/{c}' for c in encoding.values() - if c != '.notdef' - ) - ) + encoding = t1font.prop["Encoding"] + charset = "".join(sorted(f"/{c}" for c in encoding.values() if c != ".notdef")) descriptor = { - 'Type': Name('FontDescriptor'), - 'FontName': Name(t1font.prop['FontName']), - 'Flags': flags, - 'FontBBox': t1font.prop['FontBBox'], - 'ItalicAngle': italic_angle, - 'Ascent': t1font.prop['FontBBox'][3], - 'Descent': t1font.prop['FontBBox'][1], - 'CapHeight': 1000, # TODO: find this out - 'XHeight': 500, # TODO: this one too - 'FontFile': fontfileObject, - 'FontFamily': t1font.prop['FamilyName'], - 'StemV': 50, # TODO - 'CharSet': charset, + "Type": Name("FontDescriptor"), + "FontName": Name(t1font.prop["FontName"]), + "Flags": flags, + "FontBBox": t1font.prop["FontBBox"], + "ItalicAngle": italic_angle, + "Ascent": t1font.prop["FontBBox"][3], + "Descent": t1font.prop["FontBBox"][1], + "CapHeight": 1000, # TODO: find this out + "XHeight": 500, # TODO: this one too + "FontFile": fontfileObject, + "FontFamily": t1font.prop["FamilyName"], + "StemV": 50, # TODO + "CharSet": charset, # (see also revision 3874; but not all TeX distros have AFM files!) # 'FontWeight': a number where 400 = Regular, 700 = Bold } self.writeObject(fontdescObject, descriptor) - self.outputStream(fontfileObject, b"".join(t1font.parts[:2]), - extra={'Length1': len(t1font.parts[0]), - 'Length2': len(t1font.parts[1]), - 'Length3': 0}) + self.outputStream( + fontfileObject, + b"".join(t1font.parts[:2]), + extra={ + "Length1": len(t1font.parts[0]), + "Length2": len(t1font.parts[1]), + "Length3": 0, + }, + ) return fontdescObject def _get_xobject_glyph_name(self, filename, glyph_name): Fx = self.fontName(filename) - return "-".join([ - Fx.name.decode(), - os.path.splitext(os.path.basename(filename))[0], - glyph_name]) + return "-".join( + [ + Fx.name.decode(), + os.path.splitext(os.path.basename(filename))[0], + glyph_name, + ] + ) _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin @@ -1161,7 +1204,7 @@ def embedTTF(self, filename, characters): """Embed the TTF font from the named file into the document.""" font = get_font(filename) - fonttype = mpl.rcParams['pdf.fonttype'] + fonttype = mpl.rcParams["pdf.fonttype"] def cvt(length, upe=font.units_per_EM, nearest=True): """Convert font coordinates to PDF glyph coordinates.""" @@ -1176,30 +1219,28 @@ def cvt(length, upe=font.units_per_EM, nearest=True): def embedTTFType3(font, characters, descriptor): """The Type 3-specific part of embedding a Truetype font""" - widthsObject = self.reserveObject('font widths') - fontdescObject = self.reserveObject('font descriptor') - fontdictObject = self.reserveObject('font dictionary') - charprocsObject = self.reserveObject('character procs') + widthsObject = self.reserveObject("font widths") + fontdescObject = self.reserveObject("font descriptor") + fontdictObject = self.reserveObject("font dictionary") + charprocsObject = self.reserveObject("character procs") differencesArray = [] firstchar, lastchar = 0, 255 bbox = [cvt(x, nearest=False) for x in font.bbox] fontdict = { - 'Type': Name('Font'), - 'BaseFont': ps_name, - 'FirstChar': firstchar, - 'LastChar': lastchar, - 'FontDescriptor': fontdescObject, - 'Subtype': Name('Type3'), - 'Name': descriptor['FontName'], - 'FontBBox': bbox, - 'FontMatrix': [.001, 0, 0, .001, 0, 0], - 'CharProcs': charprocsObject, - 'Encoding': { - 'Type': Name('Encoding'), - 'Differences': differencesArray}, - 'Widths': widthsObject - } + "Type": Name("Font"), + "BaseFont": ps_name, + "FirstChar": firstchar, + "LastChar": lastchar, + "FontDescriptor": fontdescObject, + "Subtype": Name("Type3"), + "Name": descriptor["FontName"], + "FontBBox": bbox, + "FontMatrix": [0.001, 0, 0, 0.001, 0, 0], + "CharProcs": charprocsObject, + "Encoding": {"Type": Name("Encoding"), "Differences": differencesArray}, + "Widths": widthsObject, + } from encodings import cp1252 @@ -1207,16 +1248,20 @@ def embedTTFType3(font, characters, descriptor): def get_char_width(charcode): s = ord(cp1252.decoding_table[charcode]) width = font.load_char( - s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance + s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING + ).horiAdvance return cvt(width) + with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning # from ft2font: here we're just building the widths table, but # the missing glyphs may not even be used in the actual string. warnings.filterwarnings("ignore") - widths = [get_char_width(charcode) - for charcode in range(firstchar, lastchar+1)] - descriptor['MaxWidth'] = max(widths) + widths = [ + get_char_width(charcode) + for charcode in range(firstchar, lastchar + 1) + ] + descriptor["MaxWidth"] = max(widths) # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids @@ -1251,17 +1296,19 @@ def get_char_width(charcode): # The 2-byte characters are used as XObjects, so they # need extra info in their dictionary if charname in multi_byte_chars: - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} + charprocDict = { + "Type": Name("XObject"), + "Subtype": Name("Form"), + "BBox": bbox, + } # Each glyph includes bounding box information, # but xpdf and ghostscript can't handle it in a # Form XObject (they segfault!!!), so we remove it # from the stream here. It's not needed anyway, # since the Form XObject includes it in its BBox # value. - stream = stream[stream.find(b"d1") + 2:] - charprocObject = self.reserveObject('charProc') + stream = stream[stream.find(b"d1") + 2 :] + charprocObject = self.reserveObject("charProc") self.outputStream(charprocObject, stream, extra=charprocDict) # Send the glyphs with ccode > 255 to the XObject dictionary, @@ -1282,21 +1329,23 @@ def get_char_width(charcode): def embedTTFType42(font, characters, descriptor): """The Type 42-specific part of embedding a Truetype font""" - fontdescObject = self.reserveObject('font descriptor') - cidFontDictObject = self.reserveObject('CID font dictionary') - type0FontDictObject = self.reserveObject('Type 0 font dictionary') - cidToGidMapObject = self.reserveObject('CIDToGIDMap stream') - fontfileObject = self.reserveObject('font file stream') - wObject = self.reserveObject('Type 0 widths') - toUnicodeMapObject = self.reserveObject('ToUnicode map') + fontdescObject = self.reserveObject("font descriptor") + cidFontDictObject = self.reserveObject("CID font dictionary") + type0FontDictObject = self.reserveObject("Type 0 font dictionary") + cidToGidMapObject = self.reserveObject("CIDToGIDMap stream") + fontfileObject = self.reserveObject("font file stream") + wObject = self.reserveObject("Type 0 widths") + toUnicodeMapObject = self.reserveObject("ToUnicode map") subset_str = "".join(chr(c) for c in characters) _log.debug("SUBSET %s characters: %s", filename, subset_str) with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s %d -> %d", filename, - os.stat(filename).st_size, fontdata.getbuffer().nbytes + "SUBSET %s %d -> %d", + filename, + os.stat(filename).st_size, + fontdata.getbuffer().nbytes, ) # We need this ref for XObjects @@ -1308,49 +1357,53 @@ def embedTTFType42(font, characters, descriptor): font = FT2Font(fontdata) cidFontDict = { - 'Type': Name('Font'), - 'Subtype': Name('CIDFontType2'), - 'BaseFont': ps_name, - 'CIDSystemInfo': { - 'Registry': 'Adobe', - 'Ordering': 'Identity', - 'Supplement': 0}, - 'FontDescriptor': fontdescObject, - 'W': wObject, - 'CIDToGIDMap': cidToGidMapObject - } + "Type": Name("Font"), + "Subtype": Name("CIDFontType2"), + "BaseFont": ps_name, + "CIDSystemInfo": { + "Registry": "Adobe", + "Ordering": "Identity", + "Supplement": 0, + }, + "FontDescriptor": fontdescObject, + "W": wObject, + "CIDToGIDMap": cidToGidMapObject, + } type0FontDict = { - 'Type': Name('Font'), - 'Subtype': Name('Type0'), - 'BaseFont': ps_name, - 'Encoding': Name('Identity-H'), - 'DescendantFonts': [cidFontDictObject], - 'ToUnicode': toUnicodeMapObject - } + "Type": Name("Font"), + "Subtype": Name("Type0"), + "BaseFont": ps_name, + "Encoding": Name("Identity-H"), + "DescendantFonts": [cidFontDictObject], + "ToUnicode": toUnicodeMapObject, + } # Make fontfile stream - descriptor['FontFile2'] = fontfileObject + descriptor["FontFile2"] = fontfileObject self.outputStream( - fontfileObject, fontdata.getvalue(), - extra={'Length1': fontdata.getbuffer().nbytes}) + fontfileObject, + fontdata.getvalue(), + extra={"Length1": fontdata.getbuffer().nbytes}, + ) # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap # at the same time - cid_to_gid_map = ['\0'] * 65536 + cid_to_gid_map = ["\0"] * 65536 widths = [] max_ccode = 0 for c in characters: ccode = c gind = font.get_char_index(ccode) - glyph = font.load_char(ccode, - flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + glyph = font.load_char( + ccode, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING + ) widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) max_ccode = max(ccode, max_ccode) widths.sort() - cid_to_gid_map = cid_to_gid_map[:max_ccode + 1] + cid_to_gid_map = cid_to_gid_map[: max_ccode + 1] last_ccode = -2 w = [] @@ -1375,11 +1428,17 @@ def embedTTFType42(font, characters, descriptor): end = min(65535, end) unicode_bfrange.append( - b"<%04x> <%04x> [%s]" % - (start, end, - b" ".join(b"<%04x>" % x for x in range(start, end+1)))) - unicode_cmap = (self._identityToUnicodeCMap % - (len(unicode_groups), b"\n".join(unicode_bfrange))) + b"<%04x> <%04x> [%s]" + % ( + start, + end, + b" ".join(b"<%04x>" % x for x in range(start, end + 1)), + ) + ) + unicode_cmap = self._identityToUnicodeCMap % ( + len(unicode_groups), + b"\n".join(unicode_bfrange), + ) # Add XObjects for unsupported chars glyph_ids = [] @@ -1392,17 +1451,19 @@ def embedTTFType42(font, characters, descriptor): rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] - charprocDict = {'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'BBox': bbox} + charprocDict = { + "Type": Name("XObject"), + "Subtype": Name("Form"), + "BBox": bbox, + } # Each glyph includes bounding box information, # but xpdf and ghostscript can't handle it in a # Form XObject (they segfault!!!), so we remove it # from the stream here. It's not needed anyway, # since the Form XObject includes it in its BBox # value. - stream = stream[stream.find(b"d1") + 2:] - charprocObject = self.reserveObject('charProc') + stream = stream[stream.find(b"d1") + 2 :] + charprocObject = self.reserveObject("charProc") self.outputStream(charprocObject, stream, extra=charprocDict) name = self._get_xobject_glyph_name(filename, charname) @@ -1415,7 +1476,7 @@ def embedTTFType42(font, characters, descriptor): # ToUnicode CMap self.outputStream(toUnicodeMapObject, unicode_cmap) - descriptor['MaxWidth'] = max_width + descriptor["MaxWidth"] = max_width # Write everything out self.writeObject(cidFontDictObject, cidFontDict) @@ -1427,14 +1488,11 @@ def embedTTFType42(font, characters, descriptor): # Beginning of main embedTTF function... - ps_name = self._get_subsetted_psname( - font.postscript_name, - font.get_charmap() - ) - ps_name = ps_name.encode('ascii', 'replace') + ps_name = self._get_subsetted_psname(font.postscript_name, font.get_charmap()) + ps_name = ps_name.encode("ascii", "replace") ps_name = Name(ps_name) - pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} - post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)} + pclt = font.get_sfnt_table("pclt") or {"capHeight": 0, "xHeight": 0} + post = font.get_sfnt_table("post") or {"italicAngle": (0, 0)} ff = font.face_flags sf = font.style_flags @@ -1458,17 +1516,17 @@ def embedTTFType42(font, characters, descriptor): flags |= 1 << 18 descriptor = { - 'Type': Name('FontDescriptor'), - 'FontName': ps_name, - 'Flags': flags, - 'FontBBox': [cvt(x, nearest=False) for x in font.bbox], - 'Ascent': cvt(font.ascender, nearest=False), - 'Descent': cvt(font.descender, nearest=False), - 'CapHeight': cvt(pclt['capHeight'], nearest=False), - 'XHeight': cvt(pclt['xHeight']), - 'ItalicAngle': post['italicAngle'][1], # ??? - 'StemV': 0 # ??? - } + "Type": Name("FontDescriptor"), + "FontName": ps_name, + "Flags": flags, + "FontBBox": [cvt(x, nearest=False) for x in font.bbox], + "Ascent": cvt(font.ascender, nearest=False), + "Descent": cvt(font.descender, nearest=False), + "CapHeight": cvt(pclt["capHeight"], nearest=False), + "XHeight": cvt(pclt["xHeight"]), + "ItalicAngle": post["italicAngle"][1], # ??? + "StemV": 0, # ??? + } if fonttype == 3: return embedTTFType3(font, characters, descriptor) @@ -1483,9 +1541,10 @@ def alphaState(self, alpha): return state[0] name = next(self._alpha_state_seq) - self.alphaStates[alpha] = \ - (name, {'Type': Name('ExtGState'), - 'CA': alpha[0], 'ca': alpha[1]}) + self.alphaStates[alpha] = ( + name, + {"Type": Name("ExtGState"), "CA": alpha[0], "ca": alpha[1]}, + ) return name def _soft_mask_state(self, smask): @@ -1508,45 +1567,41 @@ def _soft_mask_state(self, smask): return state[0] name = next(self._soft_mask_seq) - groupOb = self.reserveObject('transparency group for soft mask') + groupOb = self.reserveObject("transparency group for soft mask") self._soft_mask_states[smask] = ( name, { - 'Type': Name('ExtGState'), - 'AIS': False, - 'SMask': { - 'Type': Name('Mask'), - 'S': Name('Luminosity'), - 'BC': [1], - 'G': groupOb - } - } - ) - self._soft_mask_groups.append(( - groupOb, - { - 'Type': Name('XObject'), - 'Subtype': Name('Form'), - 'FormType': 1, - 'Group': { - 'S': Name('Transparency'), - 'CS': Name('DeviceGray') + "Type": Name("ExtGState"), + "AIS": False, + "SMask": { + "Type": Name("Mask"), + "S": Name("Luminosity"), + "BC": [1], + "G": groupOb, }, - 'Matrix': [1, 0, 0, 1, 0, 0], - 'Resources': {'Shading': {'S': smask}}, - 'BBox': [0, 0, 1, 1] }, - [Name('S'), Op.shading] - )) + ) + self._soft_mask_groups.append( + ( + groupOb, + { + "Type": Name("XObject"), + "Subtype": Name("Form"), + "FormType": 1, + "Group": {"S": Name("Transparency"), "CS": Name("DeviceGray")}, + "Matrix": [1, 0, 0, 1, 0, 0], + "Resources": {"Shading": {"S": smask}}, + "BBox": [0, 0, 1, 1], + }, + [Name("S"), Op.shading], + ) + ) return name def writeExtGSTates(self): self.writeObject( self._extGStateObject, - dict([ - *self.alphaStates.values(), - *self._soft_mask_states.values() - ]) + dict([*self.alphaStates.values(), *self._soft_mask_states.values()]), ) def _write_soft_mask_groups(self): @@ -1572,45 +1627,66 @@ def hatchPattern(self, hatch_style): self._hatch_patterns[hatch_style] = name return name - hatchPatterns = _api.deprecated("3.10")(property(lambda self: { - k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() - })) + hatchPatterns = _api.deprecated("3.10")( + property( + lambda self: { + k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() + } + ) + ) def writeHatches(self): hatchDict = dict() sidelen = 72.0 for hatch_style, name in self._hatch_patterns.items(): - ob = self.reserveObject('hatch pattern') + ob = self.reserveObject("hatch pattern") hatchDict[name] = ob - res = {'Procsets': - [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]} + res = { + "Procsets": [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()] + } self.beginStream( - ob.id, None, - {'Type': Name('Pattern'), - 'PatternType': 1, 'PaintType': 1, 'TilingType': 1, - 'BBox': [0, 0, sidelen, sidelen], - 'XStep': sidelen, 'YStep': sidelen, - 'Resources': res, - # Change origin to match Agg at top-left. - 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) + ob.id, + None, + { + "Type": Name("Pattern"), + "PatternType": 1, + "PaintType": 1, + "TilingType": 1, + "BBox": [0, 0, sidelen, sidelen], + "XStep": sidelen, + "YStep": sidelen, + "Resources": res, + # Change origin to match Agg at top-left. + "Matrix": [1, 0, 0, 1, 0, self.height * 72], + }, + ) stroke_rgb, fill_rgb, hatch, lw = hatch_style - self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], - Op.setrgb_stroke) + self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_stroke) if fill_rgb is not None: - self.output(fill_rgb[0], fill_rgb[1], fill_rgb[2], - Op.setrgb_nonstroke, - 0, 0, sidelen, sidelen, Op.rectangle, - Op.fill) - self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], - Op.setrgb_nonstroke) + self.output( + fill_rgb[0], + fill_rgb[1], + fill_rgb[2], + Op.setrgb_nonstroke, + 0, + 0, + sidelen, + sidelen, + Op.rectangle, + Op.fill, + ) + self.output( + stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_nonstroke + ) self.output(lw, Op.setlinewidth) - self.output(*self.pathOperations( - Path.hatch(hatch), - Affine2D().scale(sidelen), - simplify=False)) + self.output( + *self.pathOperations( + Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False + ) + ) self.output(Op.fill_stroke) self.endStream() @@ -1634,8 +1710,8 @@ def addGouraudTriangles(self, points, colors): ------- Name, Reference """ - name = Name('GT%d' % len(self.gouraudTriangles)) - ob = self.reserveObject(f'Gouraud triangle {name}') + name = Name("GT%d" % len(self.gouraudTriangles)) + ob = self.reserveObject(f"Gouraud triangle {name}") self.gouraudTriangles.append((name, ob, points, colors)) return name, ob @@ -1653,31 +1729,36 @@ def writeGouraudTriangles(self): colordim = 3 points_min = np.min(flat_points, axis=0) - (1 << 8) points_max = np.max(flat_points, axis=0) + (1 << 8) - factor = 0xffffffff / (points_max - points_min) + factor = 0xFFFFFFFF / (points_max - points_min) self.beginStream( - ob.id, None, - {'ShadingType': 4, - 'BitsPerCoordinate': 32, - 'BitsPerComponent': 8, - 'BitsPerFlag': 8, - 'ColorSpace': Name( - 'DeviceRGB' if colordim == 3 else 'DeviceGray' - ), - 'AntiAlias': False, - 'Decode': ([points_min[0], points_max[0], - points_min[1], points_max[1]] - + [0, 1] * colordim), - }) + ob.id, + None, + { + "ShadingType": 4, + "BitsPerCoordinate": 32, + "BitsPerComponent": 8, + "BitsPerFlag": 8, + "ColorSpace": Name("DeviceRGB" if colordim == 3 else "DeviceGray"), + "AntiAlias": False, + "Decode": ( + [points_min[0], points_max[0], points_min[1], points_max[1]] + + [0, 1] * colordim + ), + }, + ) streamarr = np.empty( (shape[0] * shape[1],), - dtype=[('flags', 'u1'), - ('points', '>u4', (2,)), - ('colors', 'u1', (colordim,))]) - streamarr['flags'] = 0 - streamarr['points'] = (flat_points - points_min) * factor - streamarr['colors'] = flat_colors[:, :colordim] * 255.0 + dtype=[ + ("flags", "u1"), + ("points", ">u4", (2,)), + ("colors", "u1", (colordim,)), + ], + ) + streamarr["flags"] = 0 + streamarr["points"] = (flat_points - points_min) * factor + streamarr["colors"] = flat_colors[:, :colordim] * 255.0 self.write(streamarr.tobytes()) self.endStream() @@ -1691,7 +1772,7 @@ def imageObject(self, image): return entry[1] name = next(self._image_seq) - ob = self.reserveObject(f'image {name}') + ob = self.reserveObject(f"image {name}") self._images[id(image)] = (image, name, ob) return name @@ -1706,14 +1787,14 @@ def _unpack(self, im): return im, None else: rgb = im[:, :, :3] - rgb = np.array(rgb, order='C') + rgb = np.array(rgb, order="C") # PDF needs a separate alpha image if im.shape[2] == 4: alpha = im[:, :, 3][..., None] if np.all(alpha == 255): alpha = None else: - alpha = np.array(alpha, order='C') + alpha = np.array(alpha, order="C") else: alpha = None return rgb, alpha @@ -1726,25 +1807,25 @@ def _writePng(self, img): buffer = BytesIO() img.save(buffer, format="png") buffer.seek(8) - png_data = b'' + png_data = b"" bit_depth = palette = None while True: - length, type = struct.unpack(b'!L4s', buffer.read(8)) - if type in [b'IHDR', b'PLTE', b'IDAT']: + length, type = struct.unpack(b"!L4s", buffer.read(8)) + if type in [b"IHDR", b"PLTE", b"IDAT"]: data = buffer.read(length) if len(data) != length: raise RuntimeError("truncated data") - if type == b'IHDR': + if type == b"IHDR": bit_depth = int(data[8]) - elif type == b'PLTE': + elif type == b"PLTE": palette = data - elif type == b'IDAT': + elif type == b"IDAT": png_data += data - elif type == b'IEND': + elif type == b"IEND": break else: buffer.seek(length, 1) - buffer.seek(4, 1) # skip CRC + buffer.seek(4, 1) # skip CRC return png_data, bit_depth, palette def _writeImg(self, data, id, smask=None): @@ -1755,32 +1836,39 @@ def _writeImg(self, data, id, smask=None): width, 1)`` array. """ height, width, color_channels = data.shape - obj = {'Type': Name('XObject'), - 'Subtype': Name('Image'), - 'Width': width, - 'Height': height, - 'ColorSpace': Name({1: 'DeviceGray', 3: 'DeviceRGB'}[color_channels]), - 'BitsPerComponent': 8} + obj = { + "Type": Name("XObject"), + "Subtype": Name("Image"), + "Width": width, + "Height": height, + "ColorSpace": Name({1: "DeviceGray", 3: "DeviceRGB"}[color_channels]), + "BitsPerComponent": 8, + } if smask: - obj['SMask'] = smask - if mpl.rcParams['pdf.compression']: + obj["SMask"] = smask + if mpl.rcParams["pdf.compression"]: if data.shape[-1] == 1: data = data.squeeze(axis=-1) - png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width} + png = {"Predictor": 10, "Colors": color_channels, "Columns": width} img = Image.fromarray(data) img_colors = img.getcolors(maxcolors=256) if color_channels == 3 and img_colors is not None: # Convert to indexed color if there are 256 colors or fewer. This can # significantly reduce the file size. num_colors = len(img_colors) - palette = np.array([comp for _, color in img_colors for comp in color], - dtype=np.uint8) - palette24 = ((palette[0::3].astype(np.uint32) << 16) | - (palette[1::3].astype(np.uint32) << 8) | - palette[2::3]) - rgb24 = ((data[:, :, 0].astype(np.uint32) << 16) | - (data[:, :, 1].astype(np.uint32) << 8) | - data[:, :, 2]) + palette = np.array( + [comp for _, color in img_colors for comp in color], dtype=np.uint8 + ) + palette24 = ( + (palette[0::3].astype(np.uint32) << 16) + | (palette[1::3].astype(np.uint32) << 8) + | palette[2::3] + ) + rgb24 = ( + (data[:, :, 0].astype(np.uint32) << 16) + | (data[:, :, 1].astype(np.uint32) << 8) + | data[:, :, 2] + ) indices = np.argsort(palette24).astype(np.uint8) rgb8 = indices[np.searchsorted(palette24, rgb24, sorter=indices)] img = Image.fromarray(rgb8).convert("P") @@ -1788,22 +1876,23 @@ def _writeImg(self, data, id, smask=None): png_data, bit_depth, palette = self._writePng(img) if bit_depth is None or palette is None: raise RuntimeError("invalid PNG header") - palette = palette[:num_colors * 3] # Trim padding; remove for Pillow>=9 - obj['ColorSpace'] = [Name('Indexed'), Name('DeviceRGB'), - num_colors - 1, palette] - obj['BitsPerComponent'] = bit_depth - png['Colors'] = 1 - png['BitsPerComponent'] = bit_depth + palette = palette[ + : num_colors * 3 + ] # Trim padding; remove for Pillow>=9 + obj["ColorSpace"] = [ + Name("Indexed"), + Name("DeviceRGB"), + num_colors - 1, + palette, + ] + obj["BitsPerComponent"] = bit_depth + png["Colors"] = 1 + png["BitsPerComponent"] = bit_depth else: png_data, _, _ = self._writePng(img) else: png = None - self.beginStream( - id, - self.reserveObject('length of image stream'), - obj, - png=png - ) + self.beginStream(id, self.reserveObject("length of image stream"), obj, png=png) if png: self.currentstream.write(png_data) else: @@ -1820,8 +1909,7 @@ def writeImages(self): smaskObject = None self._writeImg(data, ob.id, smaskObject) - def markerObject(self, path, trans, fill, stroke, lw, joinstyle, - capstyle): + def markerObject(self, path, trans, fill, stroke, lw, joinstyle, capstyle): """Return name of a marker XObject representing the given path.""" # self.markers used by markerObject, writeMarkers, close: # mapping from (path operations, fill?, stroke?) to @@ -1839,8 +1927,8 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle, key = (tuple(pathops), bool(fill), bool(stroke), joinstyle, capstyle) result = self.markers.get(key) if result is None: - name = Name('M%d' % len(self.markers)) - ob = self.reserveObject('marker %d' % len(self.markers)) + name = Name("M%d" % len(self.markers)) + ob = self.reserveObject("marker %d" % len(self.markers)) bbox = path.get_extents(trans) self.markers[key] = [name, ob, bbox, lw] else: @@ -1850,8 +1938,12 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle, return name def writeMarkers(self): - for ((pathops, fill, stroke, joinstyle, capstyle), - (name, ob, bbox, lw)) in self.markers.items(): + for (pathops, fill, stroke, joinstyle, capstyle), ( + name, + ob, + bbox, + lw, + ) in self.markers.items(): # bbox wraps the exact limits of the control points, so half a line # will appear outside it. If the join style is miter and the line # is not parallel to the edge, then the line will extend even @@ -1861,28 +1953,51 @@ def writeMarkers(self): # following padding: bbox = bbox.padded(lw * 5) self.beginStream( - ob.id, None, - {'Type': Name('XObject'), 'Subtype': Name('Form'), - 'BBox': list(bbox.extents)}) - self.output(GraphicsContextPdf.joinstyles[joinstyle], - Op.setlinejoin) + ob.id, + None, + { + "Type": Name("XObject"), + "Subtype": Name("Form"), + "BBox": list(bbox.extents), + }, + ) + self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin) self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) self.output(*pathops) self.output(Op.paint_path(fill, stroke)) self.endStream() def pathCollectionObject(self, gc, path, trans, padding, filled, stroked): - name = Name('P%d' % len(self.paths)) - ob = self.reserveObject('path %d' % len(self.paths)) + name = Name("P%d" % len(self.paths)) + ob = self.reserveObject("path %d" % len(self.paths)) self.paths.append( - (name, path, trans, ob, gc.get_joinstyle(), gc.get_capstyle(), - padding, filled, stroked)) + ( + name, + path, + trans, + ob, + gc.get_joinstyle(), + gc.get_capstyle(), + padding, + filled, + stroked, + ) + ) return name def writePathCollectionTemplates(self): - for (name, path, trans, ob, joinstyle, capstyle, padding, filled, - stroked) in self.paths: - pathops = self.pathOperations(path, trans, simplify=False) + for ( + name, + path, + trans, + ob, + joinstyle, + capstyle, + padding, + filled, + stroked, + ) in self.paths: + pathops = self.pathOperations(path, trans, simplify=path.should_simplify) bbox = path.get_extents(trans) if not np.all(np.isfinite(bbox.extents)): extents = [0, 0, 0, 0] @@ -1890,11 +2005,11 @@ def writePathCollectionTemplates(self): bbox = bbox.padded(padding) extents = list(bbox.extents) self.beginStream( - ob.id, None, - {'Type': Name('XObject'), 'Subtype': Name('Form'), - 'BBox': extents}) - self.output(GraphicsContextPdf.joinstyles[joinstyle], - Op.setlinejoin) + ob.id, + None, + {"Type": Name("XObject"), "Subtype": Name("Form"), "BBox": extents}, + ) + self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin) self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) self.output(*pathops) self.output(Op.paint_path(filled, stroked)) @@ -1902,12 +2017,26 @@ def writePathCollectionTemplates(self): @staticmethod def pathOperations(path, transform, clip=None, simplify=None, sketch=None): - return [Verbatim(_path.convert_to_string( - path, transform, clip, simplify, sketch, - 6, - [Op.moveto.value, Op.lineto.value, b'', Op.curveto.value, - Op.closepath.value], - True))] + return [ + Verbatim( + _path.convert_to_string( + path, + transform, + clip, + simplify, + sketch, + 6, + [ + Op.moveto.value, + Op.lineto.value, + b"", + Op.curveto.value, + Op.closepath.value, + ], + True, + ) + ) + ] def writePath(self, path, transform, clip=False, sketch=None): if clip: @@ -1915,12 +2044,13 @@ def writePath(self, path, transform, clip=False, sketch=None): simplify = path.should_simplify else: clip = None - simplify = False - cmds = self.pathOperations(path, transform, clip, simplify=simplify, - sketch=sketch) + simplify = path.should_simplify + cmds = self.pathOperations( + path, transform, clip, simplify=simplify, sketch=sketch + ) self.output(*cmds) - def reserveObject(self, name=''): + def reserveObject(self, name=""): """ Reserve an ID for an indirect object. @@ -1944,27 +2074,31 @@ def writeXref(self): self.write(b"xref\n0 %d\n" % len(self.xrefTable)) for i, (offset, generation, name) in enumerate(self.xrefTable): if offset is None: - raise AssertionError( - 'No offset for object %d (%s)' % (i, name)) + raise AssertionError("No offset for object %d (%s)" % (i, name)) else: - key = b"f" if name == 'the zero object' else b"n" + key = b"f" if name == "the zero object" else b"n" text = b"%010d %05d %b \n" % (offset, generation, key) self.write(text) def writeInfoDict(self): """Write out the info dictionary, checking it for good form""" - self.infoObject = self.reserveObject('info') + self.infoObject = self.reserveObject("info") self.writeObject(self.infoObject, self.infoDict) def writeTrailer(self): """Write out the PDF trailer.""" self.write(b"trailer\n") - self.write(pdfRepr( - {'Size': len(self.xrefTable), - 'Root': self.rootObject, - 'Info': self.infoObject})) + self.write( + pdfRepr( + { + "Size": len(self.xrefTable), + "Root": self.rootObject, + "Info": self.infoObject, + } + ) + ) # Could add 'ID' self.write(b"\nstartxref\n%d\n%%%%EOF\n" % self.startxref) @@ -1984,10 +2118,10 @@ def finalize(self): self.file.output(*self.gc.finalize()) def check_gc(self, gc, fillcolor=None): - orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.)) + orig_fill = getattr(gc, "_fillcolor", (0.0, 0.0, 0.0)) gc._fillcolor = fillcolor - orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0)) + orig_alphas = getattr(gc, "_effective_alphas", (1.0, 1.0)) if gc.get_rgb() is None: # It should not matter what color here since linewidth should be @@ -2011,7 +2145,7 @@ def check_gc(self, gc, fillcolor=None): gc._effective_alphas = orig_alphas def get_image_magnification(self): - return self.image_dpi/72.0 + return self.image_dpi / 72.0 def draw_image(self, gc, x, y, im, transform=None): # docstring inherited @@ -2032,30 +2166,72 @@ def draw_image(self, gc, x, y, im, transform=None): imob = self.file.imageObject(im) if transform is None: - self.file.output(Op.gsave, - w, 0, 0, h, x, y, Op.concat_matrix, - imob, Op.use_xobject, Op.grestore) + self.file.output( + Op.gsave, + w, + 0, + 0, + h, + x, + y, + Op.concat_matrix, + imob, + Op.use_xobject, + Op.grestore, + ) else: tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values() - self.file.output(Op.gsave, - 1, 0, 0, 1, x, y, Op.concat_matrix, - tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix, - imob, Op.use_xobject, Op.grestore) + self.file.output( + Op.gsave, + 1, + 0, + 0, + 1, + x, + y, + Op.concat_matrix, + tr1, + tr2, + tr3, + tr4, + tr5, + tr6, + Op.concat_matrix, + imob, + Op.use_xobject, + Op.grestore, + ) def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited self.check_gc(gc, rgbFace) self.file.writePath( - path, transform, + path, + transform, rgbFace is None and gc.get_hatch_path() is None, - gc.get_sketch_params()) + gc.get_sketch_params(), + ) self.file.output(self.gc.paint()) - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + def draw_path_collection( + self, + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + *, + hatchcolors=None, + ): # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for # all of them @@ -2091,50 +2267,73 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, # uses_per_path for the uses 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 + uses_per_path + 5 < len_path * uses_per_path + paths, all_transforms, offsets, facecolors, edgecolors + ) + should_do_optimization = len_path + uses_per_path + 5 < len_path * uses_per_path if (not can_do_optimization) or (not should_do_optimization): return RendererBase.draw_path_collection( - self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=hatchcolors) + self, + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + hatchcolors=hatchcolors, + ) padding = np.max(linewidths) path_codes = [] - for i, (path, transform) in enumerate(self._iter_collection_raw_paths( - master_transform, paths, all_transforms)): + for i, (path, transform) in enumerate( + self._iter_collection_raw_paths(master_transform, paths, all_transforms) + ): name = self.file.pathCollectionObject( - gc, path, transform, padding, filled, stroked) + gc, path, transform, padding, filled, stroked + ) path_codes.append(name) output = self.file.output output(*self.gc.push()) lastx, lasty = 0, 0 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, path_codes, offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + gc, + path_codes, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + hatchcolors=hatchcolors, + ): self.check_gc(gc0, rgbFace) dx, dy = xo - lastx, yo - lasty - output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, - Op.use_xobject) + output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, Op.use_xobject) lastx, lasty = xo, yo output(*self.gc.pop()) - def draw_markers(self, gc, marker_path, marker_trans, path, trans, - rgbFace=None): + def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited # Same logic as in draw_path_collection len_marker_path = len(marker_path) uses = len(path) if len_marker_path * uses < len_marker_path + uses + 5: - RendererBase.draw_markers(self, gc, marker_path, marker_trans, - path, trans, rgbFace) + RendererBase.draw_markers( + self, gc, marker_path, marker_trans, path, trans, rgbFace + ) return self.check_gc(gc, rgbFace) @@ -2143,23 +2342,30 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, output = self.file.output marker = self.file.markerObject( - marker_path, marker_trans, fill, stroke, self.gc._linewidth, - gc.get_joinstyle(), gc.get_capstyle()) + marker_path, + marker_trans, + fill, + stroke, + self.gc._linewidth, + gc.get_joinstyle(), + gc.get_capstyle(), + ) output(Op.gsave) lastx, lasty = 0, 0 for vertices, code in path.iter_segments( - trans, - clip=(0, 0, self.file.width*72, self.file.height*72), - simplify=False): + trans, + clip=(0, 0, self.file.width * 72, self.file.height * 72), + simplify=False, + ): if len(vertices): x, y = vertices[-2:] - if not (0 <= x <= self.file.width * 72 - and 0 <= y <= self.file.height * 72): + if not ( + 0 <= x <= self.file.width * 72 and 0 <= y <= self.file.height * 72 + ): continue dx, dy = x - lastx, y - lasty - output(1, 0, 0, 1, dx, dy, Op.concat_matrix, - marker, Op.use_xobject) + output(1, 0, 0, 1, dx, dy, Op.concat_matrix, marker, Op.use_xobject) lastx, lasty = x, y output(Op.grestore) @@ -2199,37 +2405,43 @@ def draw_gouraud_triangles(self, gc, points, colors, trans): alpha = colors[:, :, 3][:, :, None] _, smask_ob = self.file.addGouraudTriangles(tpoints, alpha) gstate = self.file._soft_mask_state(smask_ob) - output(Op.gsave, gstate, Op.setgstate, - name, Op.shading, - Op.grestore) + output(Op.gsave, gstate, Op.setgstate, name, Op.shading, Op.grestore) def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0): if angle == oldangle == 0: self.file.output(x - oldx, y - oldy, Op.textpos) else: angle = math.radians(angle) - self.file.output(math.cos(angle), math.sin(angle), - -math.sin(angle), math.cos(angle), - x, y, Op.textmatrix) + self.file.output( + math.cos(angle), + math.sin(angle), + -math.sin(angle), + math.cos(angle), + x, + y, + Op.textmatrix, + ) self.file.output(0, 0, Op.textpos) def draw_mathtext(self, gc, x, y, s, prop, angle): # TODO: fix positioning and encoding - width, height, descent, glyphs, rects = \ - self._text2path.mathtext_parser.parse(s, 72, prop) + width, height, descent, glyphs, rects = self._text2path.mathtext_parser.parse( + s, 72, prop + ) if gc.get_url() is not None: - self.file._annotations[-1][1].append(_get_link_annotation( - gc, x, y, width, height, angle)) + self.file._annotations[-1][1].append( + _get_link_annotation(gc, x, y, width, height, angle) + ) - fonttype = mpl.rcParams['pdf.fonttype'] + fonttype = mpl.rcParams["pdf.fonttype"] # Set up a global transformation matrix for the whole math expression a = math.radians(angle) self.file.output(Op.gsave) - self.file.output(math.cos(a), math.sin(a), - -math.sin(a), math.cos(a), - x, y, Op.concat_matrix) + self.file.output( + math.cos(a), math.sin(a), -math.sin(a), math.cos(a), x, y, Op.concat_matrix + ) self.check_gc(gc, gc._rgb) prev_font = None, None @@ -2248,21 +2460,21 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy if (fontname, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname), fontsize, - Op.selectfont) + self.file.output( + self.file.fontName(fontname), fontsize, Op.selectfont + ) prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), - Op.show) + self.file.output(self.encode_string(chr(num), fonttype), Op.show) self.file.output(Op.end_text) for font, fontsize, ox, oy, num in unsupported_chars: - self._draw_xobject_glyph( - font, fontsize, font.get_char_index(num), ox, oy) + self._draw_xobject_glyph(font, fontsize, font.get_char_index(num), ox, oy) # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: - self.file.output(Op.gsave, ox, oy, width, height, - Op.rectangle, Op.fill, Op.grestore) + self.file.output( + Op.gsave, ox, oy, width, height, Op.rectangle, Op.fill, Op.grestore + ) # Pop off the global transformation self.file.output(Op.grestore) @@ -2273,11 +2485,12 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): fontsize = prop.get_size_in_points() dvifile = texmanager.make_dvi(s, fontsize) with dviread.Dvi(dvifile, 72) as dvi: - page, = dvi + (page,) = dvi if gc.get_url() is not None: - self.file._annotations[-1][1].append(_get_link_annotation( - gc, x, y, page.width, page.height, angle)) + self.file._annotations[-1][1].append( + _get_link_annotation(gc, x, y, page.width, page.height, angle) + ) # Gather font information and do some setup for combining # characters into strings. The variable seq will contain a @@ -2292,28 +2505,28 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): for x1, y1, dvifont, glyph, width in page.text: if dvifont != oldfont: pdfname = self.file.dviFontName(dvifont) - seq += [['font', pdfname, dvifont.size]] + seq += [["font", pdfname, dvifont.size]] oldfont = dvifont - seq += [['text', x1, y1, [bytes([glyph])], x1+width]] + seq += [["text", x1, y1, [bytes([glyph])], x1 + width]] self.file._character_tracker.track(dvifont, chr(glyph)) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one # string (if any kerns would be less than 0.1 points). i, curx, fontsize = 0, 0, None - while i < len(seq)-1: - elt, nxt = seq[i:i+2] - if elt[0] == 'font': + while i < len(seq) - 1: + elt, nxt = seq[i : i + 2] + if elt[0] == "font": fontsize = elt[2] - elif elt[0] == nxt[0] == 'text' and elt[2] == nxt[2]: + elif elt[0] == nxt[0] == "text" and elt[2] == nxt[2]: offset = elt[4] - nxt[1] if abs(offset) < 0.1: elt[3][-1] += nxt[3][0] - elt[4] += nxt[4]-nxt[1] + elt[4] += nxt[4] - nxt[1] else: - elt[3] += [offset*1000.0/fontsize, nxt[3][0]] + elt[3] += [offset * 1000.0 / fontsize, nxt[3][0]] elt[4] = nxt[4] - del seq[i+1] + del seq[i + 1] continue i += 1 @@ -2325,9 +2538,9 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): self.file.output(Op.begin_text) curx, cury, oldx, oldy = 0, 0, 0, 0 for elt in seq: - if elt[0] == 'font': + if elt[0] == "font": self.file.output(elt[1], elt[2], Op.selectfont) - elif elt[0] == 'text': + elif elt[0] == "text": curx, cury = mytrans.transform((elt[1], elt[2])) self._setup_textpos(curx, cury, angle, oldx, oldy) oldx, oldy = curx, cury @@ -2344,17 +2557,18 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): boxgc = self.new_gc() boxgc.copy_properties(gc) boxgc.set_linewidth(0) - pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, - Path.CLOSEPOLY] + pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] for x1, y1, h, w in page.boxes: - path = Path([[x1, y1], [x1+w, y1], [x1+w, y1+h], [x1, y1+h], - [0, 0]], pathops) + path = Path( + [[x1, y1], [x1 + w, y1], [x1 + w, y1 + h], [x1, y1 + h], [0, 0]], + pathops, + ) self.draw_path(boxgc, path, mytrans, gc._rgb) def encode_string(self, s, fonttype): if fonttype in (1, 3): - return s.encode('cp1252', 'replace') - return s.encode('utf-16be', 'replace') + return s.encode("cp1252", "replace") + return s.encode("utf-16be", "replace") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -2367,28 +2581,29 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fontsize = prop.get_size_in_points() - if mpl.rcParams['pdf.use14corefonts']: + if mpl.rcParams["pdf.use14corefonts"]: font = self._get_font_afm(prop) fonttype = 1 else: font = self._get_font_ttf(prop) self.file._character_tracker.track(font, s) - fonttype = mpl.rcParams['pdf.fonttype'] + fonttype = mpl.rcParams["pdf.fonttype"] if gc.get_url() is not None: font.set_text(s) width, height = font.get_width_height() - self.file._annotations[-1][1].append(_get_link_annotation( - gc, x, y, width / 64, height / 64, angle)) + self.file._annotations[-1][1].append( + _get_link_annotation(gc, x, y, width / 64, height / 64, angle) + ) # If fonttype is neither 3 nor 42, emit the whole string at once # without manual kerning. if fonttype not in [3, 42]: - self.file.output(Op.begin_text, - self.file.fontName(prop), fontsize, Op.selectfont) + self.file.output( + Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont + ) self._setup_textpos(x, y, angle) - self.file.output(self.encode_string(s, fonttype), - Op.show, Op.end_text) + self.file.output(self.encode_string(s, fonttype), Op.show, Op.end_text) # A sequence of characters is broken into multiple chunks. The chunking # serves two purposes: @@ -2426,9 +2641,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # concatenation up front self.file.output(Op.gsave) a = math.radians(angle) - self.file.output(math.cos(a), math.sin(a), - -math.sin(a), math.cos(a), - x, y, Op.concat_matrix) + self.file.output( + math.cos(a), + math.sin(a), + -math.sin(a), + math.cos(a), + x, + y, + Op.concat_matrix, + ) # Emit all the 1-byte characters in a BT/ET group. self.file.output(Op.begin_text) @@ -2440,17 +2661,21 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.file.output( # See pdf spec "Text space details" for the 1000/fontsize # (aka. 1000/T_fs) factor. - [-1000 * next(group) / fontsize if tp == float # a kern - else self.encode_string("".join(group), fonttype) - for tp, group in itertools.groupby(kerns_or_chars, type)], - Op.showkern) + [ + ( + -1000 * next(group) / fontsize + if tp == float # a kern + else self.encode_string("".join(group), fonttype) + ) + for tp, group in itertools.groupby(kerns_or_chars, type) + ], + Op.showkern, + ) prev_start_x = start_x self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. for ft_object, start_x, glyph_idx in multibyte_glyphs: - self._draw_xobject_glyph( - ft_object, fontsize, glyph_idx, start_x, 0 - ) + self._draw_xobject_glyph(ft_object, fontsize, glyph_idx, start_x, 0) self.file.output(Op.grestore) def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): @@ -2459,8 +2684,15 @@ def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, - 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, - Name(name), Op.use_xobject, + 0.001 * fontsize, + 0, + 0, + 0.001 * fontsize, + x, + y, + Op.concat_matrix, + Name(name), + Op.use_xobject, Op.grestore, ) @@ -2480,8 +2712,8 @@ def __init__(self, file): def __repr__(self): d = dict(self.__dict__) - del d['file'] - del d['parent'] + del d["file"] + del d["parent"] return repr(d) def stroke(self): @@ -2492,8 +2724,11 @@ def stroke(self): """ # _linewidth > 0: in pdf a line of width 0 is drawn at minimum # possible device width, but e.g., agg doesn't draw at all - return (self._linewidth > 0 and self._alpha > 0 and - (len(self._rgb) <= 3 or self._rgb[3] != 0.0)) + return ( + self._linewidth > 0 + and self._alpha > 0 + and (len(self._rgb) <= 3 or self._rgb[3] != 0.0) + ) def fill(self, *args): """ @@ -2506,9 +2741,9 @@ def fill(self, *args): _fillcolor = args[0] else: _fillcolor = self._fillcolor - return (self._hatch or - (_fillcolor is not None and - (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0))) + return self._hatch or ( + _fillcolor is not None and (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0) + ) def paint(self): """ @@ -2517,8 +2752,8 @@ def paint(self): """ return Op.paint_path(self.fill(), self.stroke()) - capstyles = {'butt': 0, 'round': 1, 'projecting': 2} - joinstyles = {'miter': 0, 'round': 1, 'bevel': 2} + capstyles = {"butt": 0, "round": 1, "projecting": 2} + joinstyles = {"miter": 0, "round": 1, "bevel": 2} def capstyle_cmd(self, style): return [self.capstyles[style], Op.setlinecap] @@ -2545,15 +2780,19 @@ def hatch_cmd(self, hatch, hatch_color, hatch_linewidth): if self._fillcolor is not None: return self.fillcolor_cmd(self._fillcolor) else: - return [Name('DeviceRGB'), Op.setcolorspace_nonstroke] + return [Name("DeviceRGB"), Op.setcolorspace_nonstroke] else: hatch_style = (hatch_color, self._fillcolor, hatch, hatch_linewidth) name = self.file.hatchPattern(hatch_style) - return [Name('Pattern'), Op.setcolorspace_nonstroke, - name, Op.setcolor_nonstroke] + return [ + Name("Pattern"), + Op.setcolorspace_nonstroke, + name, + Op.setcolor_nonstroke, + ] def rgb_cmd(self, rgb): - if mpl.rcParams['pdf.inheritcolor']: + if mpl.rcParams["pdf.inheritcolor"]: return [] if rgb[0] == rgb[1] == rgb[2]: return [rgb[0], Op.setgray_stroke] @@ -2561,7 +2800,7 @@ def rgb_cmd(self, rgb): return [*rgb[:3], Op.setrgb_stroke] def fillcolor_cmd(self, rgb): - if rgb is None or mpl.rcParams['pdf.inheritcolor']: + if rgb is None or mpl.rcParams["pdf.inheritcolor"]: return [] elif rgb[0] == rgb[1] == rgb[2]: return [rgb[0], Op.setgray_nonstroke] @@ -2585,34 +2824,39 @@ def clip_cmd(self, cliprect, clippath): """Set clip rectangle. Calls `.pop()` and `.push()`.""" cmds = [] # Pop graphics state until we hit the right one or the stack is empty - while ((self._cliprect, self._clippath) != (cliprect, clippath) - and self.parent is not None): + while (self._cliprect, self._clippath) != ( + cliprect, + clippath, + ) and self.parent is not None: cmds.extend(self.pop()) # Unless we hit the right one, set the clip polygon - if ((self._cliprect, self._clippath) != (cliprect, clippath) or - self.parent is None): + if (self._cliprect, self._clippath) != ( + cliprect, + clippath, + ) or self.parent is None: cmds.extend(self.push()) if self._cliprect != cliprect: cmds.extend([cliprect, Op.rectangle, Op.clip, Op.endpath]) if self._clippath != clippath: path, affine = clippath.get_transformed_path_and_affine() cmds.extend( - PdfFile.pathOperations(path, affine, simplify=False) + - [Op.clip, Op.endpath]) + PdfFile.pathOperations(path, affine, simplify=False) + + [Op.clip, Op.endpath] + ) return cmds commands = ( # must come first since may pop - (('_cliprect', '_clippath'), clip_cmd), - (('_alpha', '_forced_alpha', '_effective_alphas'), alpha_cmd), - (('_capstyle',), capstyle_cmd), - (('_fillcolor',), fillcolor_cmd), - (('_joinstyle',), joinstyle_cmd), - (('_linewidth',), linewidth_cmd), - (('_dashes',), dash_cmd), - (('_rgb',), rgb_cmd), + (("_cliprect", "_clippath"), clip_cmd), + (("_alpha", "_forced_alpha", "_effective_alphas"), alpha_cmd), + (("_capstyle",), capstyle_cmd), + (("_fillcolor",), fillcolor_cmd), + (("_joinstyle",), joinstyle_cmd), + (("_linewidth",), linewidth_cmd), + (("_dashes",), dash_cmd), + (("_rgb",), rgb_cmd), # must come after fillcolor and rgb - (('_hatch', '_hatch_color', '_hatch_linewidth'), hatch_cmd), + (("_hatch", "_hatch_color", "_hatch_linewidth"), hatch_cmd), ) def delta(self, other): @@ -2638,17 +2882,16 @@ def delta(self, other): # This should be removed when numpy < 1.25 is no longer supported. ours = np.asarray(ours) theirs = np.asarray(theirs) - different = (ours.shape != theirs.shape or - np.any(ours != theirs)) + different = ours.shape != theirs.shape or np.any(ours != theirs) if different: break # Need to update hatching if we also updated fillcolor - if cmd.__name__ == 'hatch_cmd' and fill_performed: + if cmd.__name__ == "hatch_cmd" and fill_performed: different = True if different: - if cmd.__name__ == 'fillcolor_cmd': + if cmd.__name__ == "fillcolor_cmd": fill_performed = True theirs = [getattr(other, p) for p in params] cmds.extend(cmd(self, *theirs)) @@ -2661,9 +2904,8 @@ def copy_properties(self, other): Copy properties of other into self. """ super().copy_properties(other) - fillcolor = getattr(other, '_fillcolor', self._fillcolor) - effective_alphas = getattr(other, '_effective_alphas', - self._effective_alphas) + fillcolor = getattr(other, "_fillcolor", self._fillcolor) + effective_alphas = getattr(other, "_effective_alphas", self._effective_alphas) self._fillcolor = fillcolor self._effective_alphas = effective_alphas @@ -2698,8 +2940,9 @@ class PdfPages: confusion when using `~.pyplot.savefig` and forgetting the format argument. """ - @_api.delete_parameter("3.10", "keep_empty", - addendum="This parameter does nothing.") + @_api.delete_parameter( + "3.10", "keep_empty", addendum="This parameter does nothing." + ) def __init__(self, filename, keep_empty=None, metadata=None): """ Create a new PdfPages object. @@ -2794,13 +3037,12 @@ class FigureCanvasPdf(FigureCanvasBase): # docstring inherited fixed_dpi = 72 - filetypes = {'pdf': 'Portable Document Format'} + filetypes = {"pdf": "Portable Document Format"} def get_default_filetype(self): - return 'pdf' + return "pdf" - def print_pdf(self, filename, *, - bbox_inches_restore=None, metadata=None): + def print_pdf(self, filename, *, bbox_inches_restore=None, metadata=None): dpi = self.figure.dpi self.figure.dpi = 72 # there are 72 pdf points to an inch @@ -2812,9 +3054,13 @@ def print_pdf(self, filename, *, try: file.newPage(width, height) renderer = MixedModeRenderer( - self.figure, width, height, dpi, + self.figure, + width, + height, + dpi, RendererPdf(file, dpi, height, width), - bbox_inches_restore=bbox_inches_restore) + bbox_inches_restore=bbox_inches_restore, + ) self.figure.draw(renderer) renderer.finalize() if not isinstance(filename, PdfPages): @@ -2822,7 +3068,7 @@ def print_pdf(self, filename, *, finally: if isinstance(filename, PdfPages): # finish off this page file.endStream() - else: # we opened the file above; now finish it off + else: # we opened the file above; now finish it off file.close() def draw(self): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..6edeedcb0f46 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -16,7 +16,11 @@ import matplotlib as mpl from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) + _Backend, + FigureCanvasBase, + FigureManagerBase, + RendererBase, +) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC @@ -73,12 +77,12 @@ def _escape_cdata(s): return s -_escape_xml_comment = re.compile(r'-(?=-)') +_escape_xml_comment = re.compile(r"-(?=-)") def _escape_comment(s): s = _escape_cdata(s) - return _escape_xml_comment.sub('- ', s) + return _escape_xml_comment.sub("- ", s) def _escape_attrib(s): @@ -91,9 +95,15 @@ def _escape_attrib(s): def _quote_escape_attrib(s): - return ('"' + _escape_cdata(s) + '"' if '"' not in s else - "'" + _escape_cdata(s) + "'" if "'" not in s else - '"' + _escape_attrib(s) + '"') + return ( + '"' + _escape_cdata(s) + '"' + if '"' not in s + else ( + "'" + _escape_cdata(s) + "'" + if "'" not in s + else '"' + _escape_attrib(s) + '"' + ) + ) def _short_float_fmt(x): @@ -101,7 +111,7 @@ 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 f'{x:f}'.rstrip('0').rstrip('.') + return f"{x:f}".rstrip("0").rstrip(".") class XMLWriter: @@ -129,7 +139,7 @@ def __flush(self, indent=True): self.__write(">") self.__open = 0 if self.__data: - data = ''.join(self.__data) + data = "".join(self.__data) self.__write(_escape_cdata(data)) self.__data = [] @@ -156,13 +166,13 @@ def start(self, tag, attrib={}, **extra): tag = _escape_cdata(tag) self.__data = [] self.__tags.append(tag) - self.__write(self.__indentation[:len(self.__tags) - 1]) + self.__write(self.__indentation[: len(self.__tags) - 1]) self.__write(f"<{tag}") for k, v in {**attrib, **extra}.items(): if v: k = _escape_cdata(k) v = _quote_escape_attrib(v) - self.__write(f' {k}={v}') + self.__write(f" {k}={v}") self.__open = 1 return len(self.__tags) - 1 @@ -176,7 +186,7 @@ def comment(self, comment): Comment text. """ self.__flush() - self.__write(self.__indentation[:len(self.__tags)]) + self.__write(self.__indentation[: len(self.__tags)]) self.__write(f"\n") def data(self, text): @@ -204,8 +214,9 @@ def end(self, tag=None, indent=True): """ if tag: assert self.__tags, f"unbalanced end({tag})" - assert _escape_cdata(tag) == self.__tags[-1], \ - f"expected end({self.__tags[-1]}), got {tag}" + assert ( + _escape_cdata(tag) == self.__tags[-1] + ), f"expected end({self.__tags[-1]}), got {tag}" else: assert self.__tags, "unbalanced end()" tag = self.__tags.pop() @@ -216,7 +227,7 @@ def end(self, tag=None, indent=True): self.__write("/>\n") return if indent: - self.__write(self.__indentation[:len(self.__tags)]) + self.__write(self.__indentation[: len(self.__tags)]) self.__write(f"\n") def close(self, id): @@ -251,44 +262,56 @@ def flush(self): def _generate_transform(transform_list): parts = [] 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,)): + 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): + if type == "matrix" and isinstance(value, Affine2DBase): value = value.to_values() - parts.append('{}({})'.format( - type, ' '.join(_short_float_fmt(x) for x in value))) - return ' '.join(parts) + parts.append( + "{}({})".format(type, " ".join(_short_float_fmt(x) for x in value)) + ) + return " ".join(parts) def _generate_css(attrib): return "; ".join(f"{k}: {v}" for k, v in attrib.items()) -_capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'} +_capstyle_d = {"projecting": "square", "butt": "butt", "round": "round"} def _check_is_str(info, key): if not isinstance(info, str): - raise TypeError(f'Invalid type for {key} metadata. Expected str, not ' - f'{type(info)}.') + raise TypeError( + f"Invalid type for {key} metadata. Expected str, not " f"{type(info)}." + ) def _check_is_iterable_of_str(infos, key): if np.iterable(infos): for info in infos: if not isinstance(info, str): - raise TypeError(f'Invalid type for {key} metadata. Expected ' - f'iterable of str, not {type(info)}.') + raise TypeError( + f"Invalid type for {key} metadata. Expected " + f"iterable of str, not {type(info)}." + ) else: - raise TypeError(f'Invalid type for {key} metadata. Expected str or ' - f'iterable of str, not {type(infos)}.') + raise TypeError( + f"Invalid type for {key} metadata. Expected str or " + f"iterable of str, not {type(infos)}." + ) class RendererSVG(RendererBase): - def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, - *, metadata=None): + def __init__( + self, width, height, svgwriter, basename=None, image_dpi=72, *, metadata=None + ): self.width = width self.height = height self.writer = XMLWriter(svgwriter) @@ -316,14 +339,15 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, str_width = _short_float_fmt(width) svgwriter.write(svgProlog) self._start_id = self.writer.start( - 'svg', - width=f'{str_width}pt', - height=f'{str_height}pt', - viewBox=f'0 0 {str_width} {str_height}', + "svg", + width=f"{str_width}pt", + height=f"{str_height}pt", + viewBox=f"0 0 {str_width} {str_height}", xmlns="http://www.w3.org/2000/svg", version="1.1", - id=mpl.rcParams['svg.id'], - attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) + id=mpl.rcParams["svg.id"], + attrib={"xmlns:xlink": "http://www.w3.org/1999/xlink"}, + ) self._write_metadata(metadata) self._write_default_style() @@ -354,21 +378,20 @@ def _write_metadata(self, metadata): if metadata is None: metadata = {} metadata = { - 'Format': 'image/svg+xml', - 'Type': 'http://purl.org/dc/dcmitype/StillImage', - 'Creator': - f'Matplotlib v{mpl.__version__}, https://matplotlib.org/', - **metadata + "Format": "image/svg+xml", + "Type": "http://purl.org/dc/dcmitype/StillImage", + "Creator": f"Matplotlib v{mpl.__version__}, https://matplotlib.org/", + **metadata, } writer = self.writer - if 'Title' in metadata: - title = metadata['Title'] - _check_is_str(title, 'Title') - writer.element('title', text=title) + if "Title" in metadata: + title = metadata["Title"] + _check_is_str(title, "Title") + writer.element("title", text=title) # Special handling. - date = metadata.get('Date', None) + date = metadata.get("Date", None) if date is not None: if isinstance(date, str): dates = [date] @@ -383,54 +406,70 @@ def _write_metadata(self, metadata): dates.append(d.isoformat()) else: raise TypeError( - f'Invalid type for Date metadata. ' - f'Expected iterable of str, date, or datetime, ' - f'not {type(d)}.') + f"Invalid type for Date metadata. " + f"Expected iterable of str, date, or datetime, " + f"not {type(d)}." + ) else: - raise TypeError(f'Invalid type for Date metadata. ' - f'Expected str, date, datetime, or iterable ' - f'of the same, not {type(date)}.') - metadata['Date'] = '/'.join(dates) - elif 'Date' not in metadata: + raise TypeError( + f"Invalid type for Date metadata. " + f"Expected str, date, datetime, or iterable " + f"of the same, not {type(date)}." + ) + metadata["Date"] = "/".join(dates) + elif "Date" not in metadata: # Do not add `Date` if the user explicitly set `Date` to `None` # Get source date from SOURCE_DATE_EPOCH, if set. # See https://reproducible-builds.org/specs/source-date-epoch/ date = os.getenv("SOURCE_DATE_EPOCH") if date: date = datetime.datetime.fromtimestamp(int(date), datetime.timezone.utc) - metadata['Date'] = date.replace(tzinfo=UTC).isoformat() + metadata["Date"] = date.replace(tzinfo=UTC).isoformat() else: - metadata['Date'] = datetime.datetime.today().isoformat() + metadata["Date"] = datetime.datetime.today().isoformat() mid = None + def ensure_metadata(mid): if mid is not None: return mid - mid = writer.start('metadata') - writer.start('rdf:RDF', attrib={ - 'xmlns:dc': "http://purl.org/dc/elements/1.1/", - 'xmlns:cc': "http://creativecommons.org/ns#", - 'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - }) - writer.start('cc:Work') + mid = writer.start("metadata") + writer.start( + "rdf:RDF", + attrib={ + "xmlns:dc": "http://purl.org/dc/elements/1.1/", + "xmlns:cc": "http://creativecommons.org/ns#", + "xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + }, + ) + writer.start("cc:Work") return mid - uri = metadata.pop('Type', None) + uri = metadata.pop("Type", None) if uri is not None: mid = ensure_metadata(mid) - writer.element('dc:type', attrib={'rdf:resource': uri}) + writer.element("dc:type", attrib={"rdf:resource": uri}) # Single value only. - for key in ['Title', 'Coverage', 'Date', 'Description', 'Format', - 'Identifier', 'Language', 'Relation', 'Source']: + for key in [ + "Title", + "Coverage", + "Date", + "Description", + "Format", + "Identifier", + "Language", + "Relation", + "Source", + ]: info = metadata.pop(key, None) if info is not None: mid = ensure_metadata(mid) _check_is_str(info, key) - writer.element(f'dc:{key.lower()}', text=info) + writer.element(f"dc:{key.lower()}", text=info) # Multiple Agent values. - for key in ['Creator', 'Contributor', 'Publisher', 'Rights']: + for key in ["Creator", "Contributor", "Publisher", "Rights"]: agents = metadata.pop(key, None) if agents is None: continue @@ -441,52 +480,53 @@ def ensure_metadata(mid): _check_is_iterable_of_str(agents, key) # Now we know that we have an iterable of str mid = ensure_metadata(mid) - writer.start(f'dc:{key.lower()}') + writer.start(f"dc:{key.lower()}") for agent in agents: - writer.start('cc:Agent') - writer.element('dc:title', text=agent) - writer.end('cc:Agent') - writer.end(f'dc:{key.lower()}') + writer.start("cc:Agent") + writer.element("dc:title", text=agent) + writer.end("cc:Agent") + writer.end(f"dc:{key.lower()}") # Multiple values. - keywords = metadata.pop('Keywords', None) + keywords = metadata.pop("Keywords", None) if keywords is not None: if isinstance(keywords, str): keywords = [keywords] - _check_is_iterable_of_str(keywords, 'Keywords') + _check_is_iterable_of_str(keywords, "Keywords") # Now we know that we have an iterable of str mid = ensure_metadata(mid) - writer.start('dc:subject') - writer.start('rdf:Bag') + writer.start("dc:subject") + writer.start("rdf:Bag") for keyword in keywords: - writer.element('rdf:li', text=keyword) - writer.end('rdf:Bag') - writer.end('dc:subject') + writer.element("rdf:li", text=keyword) + writer.end("rdf:Bag") + writer.end("dc:subject") if mid is not None: writer.close(mid) if metadata: - raise ValueError('Unknown metadata key(s) passed to SVG writer: ' + - ','.join(metadata)) + raise ValueError( + "Unknown metadata key(s) passed to SVG writer: " + ",".join(metadata) + ) def _write_default_style(self): writer = self.writer - default_style = _generate_css({ - 'stroke-linejoin': 'round', - 'stroke-linecap': 'butt'}) - writer.start('defs') - writer.element('style', type='text/css', text='*{%s}' % default_style) - writer.end('defs') + default_style = _generate_css( + {"stroke-linejoin": "round", "stroke-linecap": "butt"} + ) + writer.start("defs") + writer.element("style", type="text/css", text="*{%s}" % default_style) + writer.end("defs") def _make_id(self, type, content): - salt = mpl.rcParams['svg.hashsalt'] + salt = mpl.rcParams["svg.hashsalt"] if salt is None: salt = str(uuid.uuid4()) m = hashlib.sha256() - m.update(salt.encode('utf8')) - m.update(str(content).encode('utf8')) - return f'{type}{m.hexdigest()[:10]}' + m.update(salt.encode("utf8")) + m.update(str(content).encode("utf8")) + return f"{type}{m.hexdigest()[:10]}" def _make_flip_transform(self, transform): return transform + Affine2D().scale(1, -1).translate(0, self.height) @@ -504,7 +544,7 @@ def _get_hatch(self, gc, rgbFace): dictkey = (gc.get_hatch(), rgbFace, edge, lw) oid = self._hatchd.get(dictkey) if oid is None: - oid = self._make_id('h', dictkey) + oid = self._make_id("h", dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge, lw), oid) else: _, oid = oid @@ -515,44 +555,46 @@ def _write_hatches(self): return HATCH_SIZE = 72 writer = self.writer - writer.start('defs') + writer.start("defs") for (path, face, stroke, lw), oid in self._hatchd.values(): writer.start( - 'pattern', + "pattern", id=oid, patternUnits="userSpaceOnUse", - x="0", y="0", width=str(HATCH_SIZE), - height=str(HATCH_SIZE)) + 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) + Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), + simplify=False, + ) if face is None: - fill = '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) + "rect", + x="0", + y="0", + width=str(HATCH_SIZE + 1), + height=str(HATCH_SIZE + 1), + fill=fill, + ) hatch_style = { - 'fill': rgb2hex(stroke), - 'stroke': rgb2hex(stroke), - 'stroke-width': str(lw), - 'stroke-linecap': 'butt', - 'stroke-linejoin': 'miter' - } + "fill": rgb2hex(stroke), + "stroke": rgb2hex(stroke), + "stroke-width": str(lw), + "stroke-linecap": "butt", + "stroke-linejoin": "miter", + } if stroke[3] < 1: - hatch_style['stroke-opacity'] = str(stroke[3]) - writer.element( - 'path', - d=path_data, - style=_generate_css(hatch_style) - ) - writer.end('pattern') - writer.end('defs') + hatch_style["stroke-opacity"] = str(stroke[3]) + writer.element("path", d=path_data, style=_generate_css(hatch_style)) + writer.end("pattern") + writer.end("defs") def _get_style_dict(self, gc, rgbFace): """Generate a style string from the GraphicsContext and rgbFace.""" @@ -561,41 +603,43 @@ def _get_style_dict(self, gc, rgbFace): forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: - attrib['fill'] = f"url(#{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]) + attrib["fill"] = f"url(#{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' + 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]) + 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()) + 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)) + 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) + attrib["stroke"] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: - attrib['stroke-opacity'] = _short_float_fmt(rgb[3]) + 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()] + 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 @@ -610,88 +654,103 @@ def _get_clip_attrs(self, gc): dictkey = (self._get_clippath_id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds - y = self.height-(y+h) + y = self.height - (y + h) dictkey = (x, y, w, h) else: return {} clip = self._clipd.get(dictkey) if clip is None: - oid = self._make_id('p', dictkey) + 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: _, oid = clip - return {'clip-path': f'url(#{oid})'} + return {"clip-path": f"url(#{oid})"} def _write_clips(self): if not len(self._clipd): return writer = self.writer - writer.start('defs') + writer.start("defs") for clip, oid in self._clipd.values(): - writer.start('clipPath', id=oid) + 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) + 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', + "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') + 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) + self.writer.start("g", id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 - self.writer.start('g', id=f"{s}_{self._groupd[s]:d}") + self.writer.start("g", id=f"{s}_{self._groupd[s]:d}") def close_group(self, s): # docstring inherited - self.writer.end('g') + self.writer.end("g") def option_image_nocomposite(self): # docstring inherited - return not mpl.rcParams['image.composite_image'] + return not mpl.rcParams["image.composite_image"] - def _convert_path(self, path, transform=None, clip=None, simplify=None, - sketch=None): + 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') + 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 + clip = rgbFace is None and gc.get_hatch_path() is None + simplify = path.should_simplify path_data = self._convert_path( - path, trans_and_flip, clip=clip, simplify=simplify, - sketch=gc.get_sketch_params()) + path, + trans_and_flip, + clip=clip, + simplify=simplify, + sketch=gc.get_sketch_params(), + ) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) - self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), - style=self._get_style(gc, rgbFace)) + self.writer.start("a", {"xlink:href": gc.get_url()}) + self.writer.element( + "path", + d=path_data, + **self._get_clip_attrs(gc), + style=self._get_style(gc, rgbFace), + ) if gc.get_url() is not None: - self.writer.end('a') + self.writer.end("a") - def draw_markers( - self, gc, marker_path, marker_trans, path, trans, rgbFace=None): + def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited if not len(path.vertices): @@ -699,44 +758,59 @@ def draw_markers( writer = self.writer path_data = self._convert_path( - marker_path, - marker_trans + Affine2D().scale(1.0, -1.0), - simplify=False) + 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')}) + 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') + 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 - writer.start('g', **self._get_clip_attrs(gc)) + writer.start("g", **self._get_clip_attrs(gc)) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start("a", {"xlink:href": gc.get_url()}) trans_and_flip = self._make_flip_transform(trans) - attrib = {'xlink:href': f'#{oid}'} - clip = (0, 0, self.width*72, self.height*72) + attrib = {"xlink:href": f"#{oid}"} + clip = (0, 0, self.width * 72, self.height * 72) for vertices, code in path.iter_segments( - trans_and_flip, clip=clip, simplify=False): + 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) + attrib["x"] = _short_float_fmt(x) + attrib["y"] = _short_float_fmt(y) + attrib["style"] = self._get_style(gc, rgbFace) + writer.element("use", attrib=attrib) if gc.get_url() is not None: - self.writer.end('a') - writer.end('g') - - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, *, hatchcolors=None): + self.writer.end("a") + writer.end("g") + + def draw_path_collection( + self, + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + *, + hatchcolors=None, + ): if hatchcolors is None: hatchcolors = [] # Is the optimization worth it? Rough calculation: @@ -746,50 +820,75 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, # (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 = \ + 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 super().draw_path_collection( - gc, master_transform, paths, all_transforms, - offsets, offset_trans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position, hatchcolors=hatchcolors) + gc, + master_transform, + paths, + all_transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + hatchcolors=hatchcolors, + ) writer = self.writer path_codes = [] - writer.start('defs') - for i, (path, transform) in enumerate(self._iter_collection_raw_paths( - master_transform, paths, all_transforms)): + 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}_{}'.format( - self._path_collection_id, i, self._make_id('', d)) - writer.element('path', id=oid, d=d) + d = self._convert_path(path, transform, simplify=path.should_simplify) + oid = "C{:x}_{:x}_{}".format( + self._path_collection_id, i, self._make_id("", d) + ) + writer.element("path", id=oid, d=d) path_codes.append(oid) - writer.end('defs') + writer.end("defs") for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, path_codes, offsets, offset_trans, - facecolors, edgecolors, linewidths, linestyles, - antialiaseds, urls, offset_position, hatchcolors=hatchcolors): + gc, + path_codes, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + linestyles, + antialiaseds, + urls, + offset_position, + hatchcolors=hatchcolors, + ): url = gc0.get_url() if url is not None: - writer.start('a', attrib={'xlink:href': url}) + writer.start("a", attrib={"xlink:href": url}) clip_attrs = self._get_clip_attrs(gc0) if clip_attrs: - writer.start('g', **clip_attrs) + writer.start("g", **clip_attrs) attrib = { - 'xlink:href': f'#{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) + "xlink:href": f"#{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 clip_attrs: - writer.end('g') + writer.end("g") if url is not None: - writer.end('a') + writer.end("a") self._path_collection_id += 1 @@ -811,7 +910,7 @@ def _draw_gouraud_triangle(self, transformed_points, colors): return writer = self.writer - writer.start('defs') + writer.start("defs") for i in range(3): x1, y1 = transformed_points[i] x2, y2 = transformed_points[(i + 1) % 3] @@ -833,102 +932,126 @@ def _draw_gouraud_triangle(self, transformed_points, colors): yb = m2 * xb + b2 writer.start( - 'linearGradient', + "linearGradient", id=f"GR{self._n_gradients:x}_{i:d}", gradientUnits="userSpaceOnUse", - x1=_short_float_fmt(x1), y1=_short_float_fmt(y1), - x2=_short_float_fmt(xb), y2=_short_float_fmt(yb)) + x1=_short_float_fmt(x1), + y1=_short_float_fmt(y1), + x2=_short_float_fmt(xb), + y2=_short_float_fmt(yb), + ) writer.element( - 'stop', - offset='1', - style=_generate_css({ - 'stop-color': rgb2hex(avg_color), - 'stop-opacity': _short_float_fmt(rgba_color[-1])})) + "stop", + offset="1", + style=_generate_css( + { + "stop-color": rgb2hex(avg_color), + "stop-opacity": _short_float_fmt(rgba_color[-1]), + } + ), + ) writer.element( - 'stop', - offset='0', - style=_generate_css({'stop-color': rgb2hex(rgba_color), - 'stop-opacity': "0"})) + "stop", + offset="0", + style=_generate_css( + {"stop-color": rgb2hex(rgba_color), "stop-opacity": "0"} + ), + ) - writer.end('linearGradient') + writer.end("linearGradient") - writer.end('defs') + writer.end("defs") # triangle formation using "path" - dpath = (f"M {_short_float_fmt(x1)},{_short_float_fmt(y1)}" - f" L {_short_float_fmt(x2)},{_short_float_fmt(y2)}" - f" {_short_float_fmt(x3)},{_short_float_fmt(y3)} Z") + dpath = ( + f"M {_short_float_fmt(x1)},{_short_float_fmt(y1)}" + f" L {_short_float_fmt(x2)},{_short_float_fmt(y2)}" + f" {_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"}) + "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)"}) + "g", + attrib={ + "stroke": "none", + "stroke-width": "0", + "shape-rendering": "crispEdges", + "filter": "url(#colorMat)", + }, + ) writer.element( - 'path', - attrib={'d': dpath, - 'fill': f'url(#GR{self._n_gradients:x}_0)', - 'shape-rendering': "crispEdges"}) + "path", + attrib={ + "d": dpath, + "fill": f"url(#GR{self._n_gradients:x}_0)", + "shape-rendering": "crispEdges", + }, + ) writer.element( - 'path', - attrib={'d': dpath, - 'fill': f'url(#GR{self._n_gradients:x}_1)', - 'filter': 'url(#colorAdd)', - 'shape-rendering': "crispEdges"}) + "path", + attrib={ + "d": dpath, + "fill": f"url(#GR{self._n_gradients:x}_1)", + "filter": "url(#colorAdd)", + "shape-rendering": "crispEdges", + }, + ) writer.element( - 'path', - attrib={'d': dpath, - 'fill': f'url(#GR{self._n_gradients:x}_2)', - 'filter': 'url(#colorAdd)', - 'shape-rendering': "crispEdges"}) + "path", + attrib={ + "d": dpath, + "fill": f"url(#GR{self._n_gradients:x}_2)", + "filter": "url(#colorAdd)", + "shape-rendering": "crispEdges", + }, + ) - writer.end('g') + writer.end("g") self._n_gradients += 1 - def draw_gouraud_triangles(self, gc, triangles_array, colors_array, - transform): + def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): writer = self.writer - writer.start('g', **self._get_clip_attrs(gc)) + writer.start("g", **self._get_clip_attrs(gc)) transform = transform.frozen() trans_and_flip = self._make_flip_transform(transform) if not self._has_gouraud: self._has_gouraud = True - writer.start( - 'filter', - id='colorAdd') + writer.start("filter", id="colorAdd") writer.element( - 'feComposite', - attrib={'in': 'SourceGraphic'}, - in2='BackgroundImage', - operator='arithmetic', - k2="1", k3="1") - writer.end('filter') + "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.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') + "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") for points, colors in zip(triangles_array, colors_array): self._draw_gouraud_triangle(trans_and_flip.transform(points), colors) - writer.end('g') + writer.end("g") def option_scale_image(self): # docstring inherited @@ -949,71 +1072,76 @@ def draw_image(self, gc, x, y, im, transform=None): if clip_attrs: # 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', **clip_attrs) + self.writer.start("g", **clip_attrs) url = gc.get_url() if url is not None: - self.writer.start('a', attrib={'xlink:href': url}) + self.writer.start("a", attrib={"xlink:href": url}) attrib = {} oid = gc.get_gid() - if mpl.rcParams['svg.image_inline']: + if mpl.rcParams["svg.image_inline"]: buf = BytesIO() Image.fromarray(im).save(buf, format="png") - oid = oid or self._make_id('image', buf.getvalue()) - attrib['xlink:href'] = ( - "data:image/png;base64,\n" + - base64.b64encode(buf.getvalue()).decode('ascii')) + oid = oid or self._make_id("image", buf.getvalue()) + attrib["xlink:href"] = "data:image/png;base64,\n" + base64.b64encode( + buf.getvalue() + ).decode("ascii") else: if self.basename is None: - raise ValueError("Cannot save image data to filesystem when " - "writing SVG to an in-memory buffer") - filename = f'{self.basename}.image{next(self._image_counter)}.png' - _log.info('Writing image file for inclusion: %s', filename) + raise ValueError( + "Cannot save image data to filesystem when " + "writing SVG to an in-memory buffer" + ) + filename = f"{self.basename}.image{next(self._image_counter)}.png" + _log.info("Writing image file for inclusion: %s", filename) Image.fromarray(im).save(filename) - oid = oid or 'Im_' + self._make_id('image', filename) - attrib['xlink:href'] = filename - attrib['id'] = oid + 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))]), + "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) + 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) + attrib["opacity"] = _short_float_fmt(alpha) flipped = ( - Affine2D().scale(1.0 / w, 1.0 / h) + - transform + - Affine2D() + Affine2D().scale(1.0 / w, 1.0 / h) + + transform + + Affine2D() .translate(x, y) .scale(1.0, -1.0) - .translate(0.0, self.height)) + .translate(0.0, self.height) + ) - attrib['transform'] = _generate_transform( - [('matrix', flipped.frozen())]) - attrib['style'] = ( - 'image-rendering:crisp-edges;' - 'image-rendering:pixelated') + attrib["transform"] = _generate_transform([("matrix", flipped.frozen())]) + attrib["style"] = "image-rendering:crisp-edges;" "image-rendering:pixelated" self.writer.element( - 'image', - width=_short_float_fmt(w), height=_short_float_fmt(h), - attrib=attrib) + "image", + width=_short_float_fmt(w), + height=_short_float_fmt(h), + attrib=attrib, + ) if url is not None: - self.writer.end('a') + self.writer.end("a") if clip_attrs: - self.writer.end('g') + self.writer.end("g") def _update_glyph_map_defs(self, glyph_map_new): """ @@ -1022,16 +1150,20 @@ def _update_glyph_map_defs(self, glyph_map_new): """ writer = self.writer if glyph_map_new: - writer.start('defs') + writer.start("defs") for char_id, (vertices, codes) in glyph_map_new.items(): char_id = self._adjust_char_id(char_id) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( - Path(vertices * 64, codes), simplify=False) + Path(vertices * 64, codes), simplify=False + ) writer.element( - 'path', id=char_id, d=path_data, - transform=_generate_transform([('scale', (1 / 64,))])) - writer.end('defs') + "path", + id=char_id, + d=path_data, + transform=_generate_transform([("scale", (1 / 64,))]), + ) + writer.end("defs") self._glyph_map.update(glyph_map_new) def _adjust_char_id(self, char_id): @@ -1050,63 +1182,75 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): fontsize = prop.get_size_in_points() style = {} - if color != '#000000': - style['fill'] = color + 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) + style["opacity"] = _short_float_fmt(alpha) font_scale = fontsize / text2path.FONT_SCALE attrib = { - 'style': _generate_css(style), - 'transform': _generate_transform([ - ('translate', (x, y)), - ('rotate', (-angle,)), - ('scale', (font_scale, -font_scale))]), + "style": _generate_css(style), + "transform": _generate_transform( + [ + ("translate", (x, y)), + ("rotate", (-angle,)), + ("scale", (font_scale, -font_scale)), + ] + ), } - writer.start('g', attrib=attrib) + writer.start("g", attrib=attrib) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( - font, s, glyph_map=glyph_map, return_new_glyphs_only=True) + font, s, glyph_map=glyph_map, return_new_glyphs_only=True + ) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: writer.element( - 'use', - transform=_generate_transform([ - ('translate', (xposition, yposition)), - ('scale', (scale,)), - ]), - attrib={'xlink:href': f'#{glyph_id}'}) + "use", + transform=_generate_transform( + [ + ("translate", (xposition, yposition)), + ("scale", (scale,)), + ] + ), + attrib={"xlink:href": f"#{glyph_id}"}, + ) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex( - prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) + 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) + prop, s, glyph_map=glyph_map, return_new_glyphs_only=True + ) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element( - 'use', - transform=_generate_transform([ - ('translate', (xposition, yposition)), - ('scale', (scale,)), - ]), - attrib={'xlink:href': f'#{char_id}'}) + "use", + transform=_generate_transform( + [ + ("translate", (xposition, yposition)), + ("scale", (scale,)), + ] + ), + attrib={"xlink:href": f"#{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.element("path", d=path_data) - writer.end('g') + writer.end("g") def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): # NOTE: If you change the font styling CSS, then be sure the check for @@ -1119,27 +1263,27 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): color = rgb2hex(gc.get_rgb()) font_style = {} color_style = {} - if color != '#000000': - color_style['fill'] = color + if color != "#000000": + color_style["fill"] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - color_style['opacity'] = _short_float_fmt(alpha) + color_style["opacity"] = _short_float_fmt(alpha) if not ismath: attrib = {} # Separate font style in their separate attributes - if prop.get_style() != 'normal': - font_style['font-style'] = prop.get_style() - if prop.get_variant() != 'normal': - font_style['font-variant'] = prop.get_variant() + if prop.get_style() != "normal": + font_style["font-style"] = prop.get_style() + if prop.get_variant() != "normal": + font_style["font-variant"] = prop.get_variant() weight = fm.weight_dict[prop.get_weight()] if weight != 400: - font_style['font-weight'] = f'{weight}' + font_style["font-weight"] = f"{weight}" def _normalize_sans(name): - return 'sans-serif' if name in ['sans', 'sans serif'] else name + return "sans-serif" if name in ["sans", "sans serif"] else name def _expand_family_entry(fn): fn = _normalize_sans(fn) @@ -1155,19 +1299,21 @@ def _expand_family_entry(fn): def _get_all_quoted_names(prop): # only quote specific names, not generic names - return [name if name in fm.font_family_aliases else repr(name) - for entry in prop.get_family() - for name in _expand_family_entry(entry)] + return [ + name if name in fm.font_family_aliases else repr(name) + for entry in prop.get_family() + for name in _expand_family_entry(entry) + ] - font_style['font-size'] = f'{_short_float_fmt(prop.get_size())}px' + font_style["font-size"] = f"{_short_float_fmt(prop.get_size())}px" # ensure expansion, quoting, and dedupe of font names - font_style['font-family'] = ", ".join( + font_style["font-family"] = ", ".join( dict.fromkeys(_get_all_quoted_names(prop)) - ) + ) - if prop.get_stretch() != 'normal': - font_style['font-stretch'] = prop.get_stretch() - attrib['style'] = _generate_css({**font_style, **color_style}) + if prop.get_stretch() != "normal": + font_style["font-stretch"] = prop.get_stretch() + attrib["style"] = _generate_css({**font_style, **color_style}) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original @@ -1187,39 +1333,41 @@ def _get_all_quoted_names(prop): ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] - ha_mpl_to_svg = {'left': 'start', 'right': 'end', - 'center': 'middle'} - font_style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] + ha_mpl_to_svg = {"left": "start", "right": "end", "center": "middle"} + font_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({**font_style, **color_style}) - attrib['transform'] = _generate_transform([ - ("rotate", (-angle, ax, ay))]) + attrib["x"] = _short_float_fmt(ax) + attrib["y"] = _short_float_fmt(ay) + attrib["style"] = _generate_css({**font_style, **color_style}) + attrib["transform"] = _generate_transform( + [("rotate", (-angle, ax, ay))] + ) else: - attrib['transform'] = _generate_transform([ - ('translate', (x, y)), - ('rotate', (-angle,))]) + attrib["transform"] = _generate_transform( + [("translate", (x, y)), ("rotate", (-angle,))] + ) - writer.element('text', s, attrib=attrib) + writer.element("text", s, attrib=attrib) else: writer.comment(s) - width, height, descent, glyphs, rects = \ + width, height, descent, glyphs, rects = ( self._text2path.mathtext_parser.parse(s, 72, prop) + ) # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. - writer.start('g', - style=_generate_css({**font_style, **color_style}), - transform=_generate_transform([ - ('translate', (x, y)), - ('rotate', (-angle,))]), - ) + writer.start( + "g", + style=_generate_css({**font_style, **color_style}), + transform=_generate_transform( + [("translate", (x, y)), ("rotate", (-angle,))] + ), + ) - writer.start('text') + writer.start("text") # Sort the characters by font, and output one tspan for each. spans = {} @@ -1227,43 +1375,44 @@ def _get_all_quoted_names(prop): entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes - if entry.style != 'normal': - font_style['font-style'] = entry.style - if entry.variant != 'normal': - font_style['font-variant'] = entry.variant + if entry.style != "normal": + font_style["font-style"] = entry.style + if entry.variant != "normal": + font_style["font-variant"] = entry.variant if entry.weight != 400: - font_style['font-weight'] = f'{entry.weight}' - font_style['font-size'] = f'{_short_float_fmt(fontsize)}px' - font_style['font-family'] = f'{entry.name!r}' # ensure quoting - if entry.stretch != 'normal': - font_style['font-stretch'] = entry.stretch + font_style["font-weight"] = f"{entry.weight}" + font_style["font-size"] = f"{_short_float_fmt(fontsize)}px" + font_style["font-family"] = f"{entry.name!r}" # ensure quoting + if entry.stretch != "normal": + font_style["font-stretch"] = entry.stretch style = _generate_css({**font_style, **color_style}) if thetext == 32: - thetext = 0xa0 # non-breaking space + thetext = 0xA0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position for x, y, t in chars: # Output one tspan for each character writer.element( - 'tspan', + "tspan", chr(t), x=_short_float_fmt(x), y=_short_float_fmt(y), - style=style) + style=style, + ) - writer.end('text') + writer.end("text") for x, y, width, height in rects: writer.element( - 'rect', + "rect", x=_short_float_fmt(x), - y=_short_float_fmt(-y-1), + y=_short_float_fmt(-y - 1), width=_short_float_fmt(width), - height=_short_float_fmt(height) - ) + height=_short_float_fmt(height), + ) - writer.end('g') + writer.end("g") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -1272,21 +1421,21 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if clip_attrs: # Cannot apply clip-path directly to the text, because # it has a transformation - self.writer.start('g', **clip_attrs) + self.writer.start("g", **clip_attrs) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start("a", {"xlink:href": gc.get_url()}) - if mpl.rcParams['svg.fonttype'] == 'path': + if mpl.rcParams["svg.fonttype"] == "path": self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: - self.writer.end('a') + self.writer.end("a") if clip_attrs: - self.writer.end('g') + self.writer.end("g") def flipy(self): # docstring inherited @@ -1302,8 +1451,7 @@ def get_text_width_height_descent(self, s, prop, ismath): class FigureCanvasSVG(FigureCanvasBase): - filetypes = {'svg': 'Scalable Vector Graphics', - 'svgz': 'Scalable Vector Graphics'} + filetypes = {"svg": "Scalable Vector Graphics", "svgz": "Scalable Vector Graphics"} fixed_dpi = 72 @@ -1341,25 +1489,31 @@ def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None): """ with cbook.open_file_cm(filename, "w", encoding="utf-8") as fh: if not cbook.file_requires_unicode(fh): - fh = codecs.getwriter('utf-8')(fh) + fh = codecs.getwriter("utf-8")(fh) dpi = self.figure.dpi self.figure.dpi = 72 width, height = self.figure.get_size_inches() w, h = width * 72, height * 72 renderer = MixedModeRenderer( - self.figure, width, height, dpi, + self.figure, + width, + height, + dpi, RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata), - bbox_inches_restore=bbox_inches_restore) + bbox_inches_restore=bbox_inches_restore, + ) self.figure.draw(renderer) renderer.finalize() def print_svgz(self, filename, **kwargs): - with (cbook.open_file_cm(filename, "wb") as fh, - gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter): + with ( + cbook.open_file_cm(filename, "wb") as fh, + gzip.GzipFile(mode="w", fileobj=fh) as gzipwriter, + ): return self.print_svg(gzipwriter, **kwargs) def get_default_filetype(self): - return 'svg' + return "svg" def draw(self): self.figure.draw_without_rendering() diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index f65ade669167..4ec78758e598 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -76,27 +76,31 @@ class Path: made up front in the constructor that will not change when the data changes. """ + code_type = np.uint8 # Path codes - STOP = code_type(0) # 1 vertex - MOVETO = code_type(1) # 1 vertex - LINETO = code_type(2) # 1 vertex - CURVE3 = code_type(3) # 2 vertices - CURVE4 = code_type(4) # 3 vertices - CLOSEPOLY = code_type(79) # 1 vertex + STOP = code_type(0) # 1 vertex + MOVETO = code_type(1) # 1 vertex + LINETO = code_type(2) # 1 vertex + CURVE3 = code_type(3) # 2 vertices + CURVE4 = code_type(4) # 3 vertices + CLOSEPOLY = code_type(79) # 1 vertex #: A dictionary mapping Path codes to the number of vertices that the #: code expects. - NUM_VERTICES_FOR_CODE = {STOP: 1, - MOVETO: 1, - LINETO: 1, - CURVE3: 2, - CURVE4: 3, - CLOSEPOLY: 1} - - def __init__(self, vertices, codes=None, _interpolation_steps=1, - closed=False, readonly=False): + NUM_VERTICES_FOR_CODE = { + STOP: 1, + MOVETO: 1, + LINETO: 1, + CURVE3: 2, + CURVE4: 3, + CLOSEPOLY: 1, + } + + def __init__( + self, vertices, codes=None, _interpolation_steps=1, closed=False, readonly=False + ): """ Create a new path with the given vertices and codes. @@ -131,14 +135,18 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, if codes is not None and len(vertices): codes = np.asarray(codes, self.code_type) if codes.ndim != 1 or len(codes) != len(vertices): - raise ValueError("'codes' must be a 1D list or array with the " - "same length of 'vertices'. " - f"Your vertices have shape {vertices.shape} " - f"but your codes have shape {codes.shape}") + raise ValueError( + "'codes' must be a 1D list or array with the " + "same length of 'vertices'. " + f"Your vertices have shape {vertices.shape} " + f"but your codes have shape {codes.shape}" + ) if len(codes) and codes[0] != self.MOVETO: - raise ValueError("The first element of 'code' must be equal " - f"to 'MOVETO' ({self.MOVETO}). " - f"Your first code is {codes[0]}") + raise ValueError( + "The first element of 'code' must be equal " + f"to 'MOVETO' ({self.MOVETO}). " + f"Your first code is {codes[0]}" + ) elif closed and len(vertices): codes = np.empty(len(vertices), dtype=self.code_type) codes[0] = self.MOVETO @@ -183,7 +191,7 @@ def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): pth._interpolation_steps = internals_from._interpolation_steps else: pth._should_simplify = True - pth._simplify_threshold = mpl.rcParams['path.simplify_threshold'] + pth._simplify_threshold = mpl.rcParams["path.simplify_threshold"] pth._interpolation_steps = 1 return pth @@ -199,12 +207,17 @@ def _create_closed(cls, vertices): return cls(np.concatenate([v, v[:1]]), closed=True) def _update_values(self): - self._simplify_threshold = mpl.rcParams['path.simplify_threshold'] + self._simplify_threshold = mpl.rcParams["path.simplify_threshold"] self._should_simplify = ( - self._simplify_threshold > 0 and - mpl.rcParams['path.simplify'] and - len(self._vertices) >= 128 and - (self._codes is None or np.all(self._codes <= Path.LINETO)) + self._simplify_threshold > 0 + and mpl.rcParams["path.simplify"] + and len(self._vertices) >= 128 + and ( + self._codes is None + or np.all( + (self._codes <= Path.LINETO) | (self._codes == Path.CLOSEPOLY) + ) + ) ) @property @@ -350,9 +363,9 @@ def make_compound_path(cls, *args): if path.codes is None: if size: codes[i] = cls.MOVETO - codes[i+1:i+size] = cls.LINETO + codes[i + 1 : i + size] = cls.LINETO else: - codes[i:i+size] = path.codes + codes[i : i + size] = path.codes i += size not_stop_mask = codes != cls.STOP # Remove STOPs, as internal STOPs are a bug. return cls(vertices[not_stop_mask], codes[not_stop_mask]) @@ -363,9 +376,17 @@ def __repr__(self): def __len__(self): return len(self.vertices) - def iter_segments(self, transform=None, remove_nans=True, clip=None, - snap=False, stroke_width=1.0, simplify=None, - curves=True, sketch=None): + def iter_segments( + self, + transform=None, + remove_nans=True, + clip=None, + snap=False, + stroke_width=1.0, + simplify=None, + curves=True, + sketch=None, + ): """ Iterate over all curve segments in the path. @@ -407,11 +428,16 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None, if not len(self): return - cleaned = self.cleaned(transform=transform, - remove_nans=remove_nans, clip=clip, - snap=snap, stroke_width=stroke_width, - simplify=simplify, curves=curves, - sketch=sketch) + cleaned = self.cleaned( + transform=transform, + remove_nans=remove_nans, + clip=clip, + snap=snap, + stroke_width=stroke_width, + simplify=simplify, + curves=curves, + sketch=sketch, + ) # Cache these object lookups for performance in the loop. NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE @@ -463,11 +489,11 @@ def iter_bezier(self, **kwargs): elif code == Path.LINETO: # "CURVE2" yield BezierSegment(np.array([prev_vert, verts])), code elif code == Path.CURVE3: - yield BezierSegment(np.array([prev_vert, verts[:2], - verts[2:]])), code + yield BezierSegment(np.array([prev_vert, verts[:2], verts[2:]])), code elif code == Path.CURVE4: - yield BezierSegment(np.array([prev_vert, verts[:2], - verts[2:4], verts[4:]])), code + yield BezierSegment( + np.array([prev_vert, verts[:2], verts[2:4], verts[4:]]) + ), code elif code == Path.CLOSEPOLY: yield BezierSegment(np.array([prev_vert, first_vert])), code elif code == Path.STOP: @@ -484,11 +510,21 @@ def _iter_connected_components(self): idxs = np.append((self.codes == Path.MOVETO).nonzero()[0], len(self.codes)) for sl in map(slice, idxs, idxs[1:]): yield Path._fast_from_codes_and_verts( - self.vertices[sl], self.codes[sl], self) - - def cleaned(self, transform=None, remove_nans=False, clip=None, - *, simplify=False, curves=False, - stroke_width=1.0, snap=False, sketch=None): + self.vertices[sl], self.codes[sl], self + ) + + def cleaned( + self, + transform=None, + remove_nans=False, + clip=None, + *, + simplify=False, + curves=False, + stroke_width=1.0, + snap=False, + sketch=None, + ): """ Return a new `Path` with vertices and codes cleaned according to the parameters. @@ -498,8 +534,16 @@ def cleaned(self, transform=None, remove_nans=False, clip=None, Path.iter_segments : for details of the keyword arguments. """ vertices, codes = _path.cleanup_path( - self, transform, remove_nans, clip, snap, stroke_width, simplify, - curves, sketch) + self, + transform, + remove_nans, + clip, + snap, + stroke_width, + simplify, + curves, + sketch, + ) pth = Path._fast_from_codes_and_verts(vertices, codes, self) if not simplify: pth._should_simplify = False @@ -515,8 +559,9 @@ def transformed(self, transform): A specialized path class that will cache the transformed result and automatically update when the transform changes. """ - return Path(transform.transform(self.vertices), self.codes, - self._interpolation_steps) + return Path( + transform.transform(self.vertices), self.codes, self._interpolation_steps + ) def contains_point(self, point, transform=None, radius=0.0): """ @@ -610,7 +655,7 @@ def contains_points(self, points, transform=None, radius=0.0): if transform is not None: transform = transform.frozen() result = _path.points_in_path(points, radius, self, transform) - return result.astype('bool') + return result.astype("bool") def contains_path(self, path, transform=None): """ @@ -640,6 +685,7 @@ def get_extents(self, transform=None, **kwargs): The extents of the path Bbox([[xmin, ymin], [xmax, ymax]]) """ from .transforms import Bbox + if transform is not None: self = transform.transform_path(self) if self.codes is None: @@ -649,8 +695,7 @@ def get_extents(self, transform=None, **kwargs): # Instead of iterating through each curve, consider # each line segment's end-points # (recall that STOP and CLOSEPOLY vertices are ignored) - xys = self.vertices[np.isin(self.codes, - [Path.MOVETO, Path.LINETO])] + xys = self.vertices[np.isin(self.codes, [Path.MOVETO, Path.LINETO])] else: xys = [] for curve, code in self.iter_bezier(**kwargs): @@ -683,7 +728,8 @@ def intersects_bbox(self, bbox, filled=True): The bounding box is always considered filled. """ return _path.path_intersects_rectangle( - self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled) + self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled + ) def interpolated(self, steps): """ @@ -706,10 +752,16 @@ def interpolated(self, steps): if self.codes is not None and self.MOVETO in self.codes[1:]: return self.make_compound_path( - *(p.interpolated(steps) for p in self._iter_connected_components())) - - if self.codes is not None and self.CLOSEPOLY in self.codes and not np.all( - self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0]): + *(p.interpolated(steps) for p in self._iter_connected_components()) + ) + + if ( + self.codes is not None + and self.CLOSEPOLY in self.codes + and not np.all( + self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0] + ) + ): vertices = self.vertices.copy() vertices[self.codes == self.CLOSEPOLY] = vertices[0] else: @@ -718,8 +770,9 @@ def interpolated(self, steps): vertices = simple_linear_interpolation(vertices, steps) codes = self.codes if codes is not None: - new_codes = np.full((len(codes) - 1) * steps + 1, Path.LINETO, - dtype=self.code_type) + new_codes = np.full( + (len(codes) - 1) * steps + 1, Path.LINETO, dtype=self.code_type + ) new_codes[0::steps] = codes else: new_codes = None @@ -770,7 +823,8 @@ def to_polygons(self, transform=None, width=0, height=0, closed_only=True): # Deal with the case where there are curves and/or multiple # subpaths (using extension code) return _path.convert_path_to_polygons( - self, transform, width, height, closed_only) + self, transform, width, height, closed_only + ) _unit_rectangle = None @@ -780,8 +834,9 @@ def unit_rectangle(cls): Return a `Path` instance of the unit rectangle from (0, 0) to (1, 1). """ if cls._unit_rectangle is None: - cls._unit_rectangle = cls([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], - closed=True, readonly=True) + cls._unit_rectangle = cls( + [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], closed=True, readonly=True + ) return cls._unit_rectangle _unit_regular_polygons = WeakValueDictionary() @@ -798,10 +853,12 @@ def unit_regular_polygon(cls, numVertices): else: path = None if path is None: - theta = ((2 * np.pi / numVertices) * np.arange(numVertices + 1) - # This initial rotation is to make sure the polygon always - # "points-up". - + np.pi / 2) + theta = ( + (2 * np.pi / numVertices) * np.arange(numVertices + 1) + # This initial rotation is to make sure the polygon always + # "points-up". + + np.pi / 2 + ) verts = np.column_stack((np.cos(theta), np.sin(theta))) path = cls(verts, closed=True, readonly=True) if numVertices <= 16: @@ -822,7 +879,7 @@ def unit_regular_star(cls, numVertices, innerCircle=0.5): path = None if path is None: ns2 = numVertices * 2 - theta = (2*np.pi/ns2 * np.arange(ns2 + 1)) + theta = 2 * np.pi / ns2 * np.arange(ns2 + 1) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 @@ -852,12 +909,11 @@ def unit_circle(cls): For most cases, :func:`Path.circle` will be what you want. """ if cls._unit_circle is None: - cls._unit_circle = cls.circle(center=(0, 0), radius=1, - readonly=True) + cls._unit_circle = cls.circle(center=(0, 0), radius=1, readonly=True) return cls._unit_circle @classmethod - def circle(cls, center=(0., 0.), radius=1., readonly=False): + def circle(cls, center=(0.0, 0.0), radius=1.0, readonly=False): """ Return a `Path` representing a circle of a given radius and center. @@ -882,42 +938,37 @@ def circle(cls, center=(0., 0.), radius=1., readonly=False): SQRTHALF = np.sqrt(0.5) MAGIC45 = SQRTHALF * MAGIC - vertices = np.array([[0.0, -1.0], - - [MAGIC, -1.0], - [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], - [SQRTHALF, -SQRTHALF], - - [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], - [1.0, -MAGIC], - [1.0, 0.0], - - [1.0, MAGIC], - [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], - [SQRTHALF, SQRTHALF], - - [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], - [MAGIC, 1.0], - [0.0, 1.0], - - [-MAGIC, 1.0], - [-SQRTHALF+MAGIC45, SQRTHALF+MAGIC45], - [-SQRTHALF, SQRTHALF], - - [-SQRTHALF-MAGIC45, SQRTHALF-MAGIC45], - [-1.0, MAGIC], - [-1.0, 0.0], - - [-1.0, -MAGIC], - [-SQRTHALF-MAGIC45, -SQRTHALF+MAGIC45], - [-SQRTHALF, -SQRTHALF], - - [-SQRTHALF+MAGIC45, -SQRTHALF-MAGIC45], - [-MAGIC, -1.0], - [0.0, -1.0], - - [0.0, -1.0]], - dtype=float) + vertices = np.array( + [ + [0.0, -1.0], + [MAGIC, -1.0], + [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], + [SQRTHALF, -SQRTHALF], + [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], + [1.0, -MAGIC], + [1.0, 0.0], + [1.0, MAGIC], + [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], + [SQRTHALF, SQRTHALF], + [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], + [MAGIC, 1.0], + [0.0, 1.0], + [-MAGIC, 1.0], + [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45], + [-SQRTHALF, SQRTHALF], + [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45], + [-1.0, MAGIC], + [-1.0, 0.0], + [-1.0, -MAGIC], + [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45], + [-SQRTHALF, -SQRTHALF], + [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45], + [-MAGIC, -1.0], + [0.0, -1.0], + [0.0, -1.0], + ], + dtype=float, + ) codes = [cls.CURVE4] * 26 codes[0] = cls.MOVETO @@ -939,27 +990,24 @@ def unit_circle_righthalf(cls): MAGIC45 = SQRTHALF * MAGIC vertices = np.array( - [[0.0, -1.0], - - [MAGIC, -1.0], - [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], - [SQRTHALF, -SQRTHALF], - - [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], - [1.0, -MAGIC], - [1.0, 0.0], - - [1.0, MAGIC], - [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], - [SQRTHALF, SQRTHALF], - - [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], - [MAGIC, 1.0], - [0.0, 1.0], - - [0.0, -1.0]], - - float) + [ + [0.0, -1.0], + [MAGIC, -1.0], + [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], + [SQRTHALF, -SQRTHALF], + [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], + [1.0, -MAGIC], + [1.0, 0.0], + [1.0, MAGIC], + [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], + [SQRTHALF, SQRTHALF], + [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], + [MAGIC, 1.0], + [0.0, 1.0], + [0.0, -1.0], + ], + float, + ) codes = np.full(14, cls.CURVE4, dtype=cls.code_type) codes[0] = cls.MOVETO @@ -1040,10 +1088,10 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False): vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot - vertices[vertex_offset+1:end:3, 0] = xB - alpha * xB_dot - vertices[vertex_offset+1:end:3, 1] = yB - alpha * yB_dot - vertices[vertex_offset+2:end:3, 0] = xB - vertices[vertex_offset+2:end:3, 1] = yB + vertices[vertex_offset + 1 : end : 3, 0] = xB - alpha * xB_dot + vertices[vertex_offset + 1 : end : 3, 1] = yB - alpha * yB_dot + vertices[vertex_offset + 2 : end : 3, 0] = xB + vertices[vertex_offset + 2 : end : 3, 1] = yB return cls(vertices, codes, readonly=True) @@ -1074,8 +1122,8 @@ def hatch(hatchpattern, density=6): number of lines per unit square. """ from matplotlib.hatch import get_path - return (get_path(hatchpattern, density) - if hatchpattern is not None else None) + + return get_path(hatchpattern, density) if hatchpattern is not None else None def clip_to_bbox(self, bbox, inside=True): """ @@ -1093,7 +1141,8 @@ def clip_to_bbox(self, bbox, inside=True): def get_path_collection_extents( - master_transform, paths, transforms, offsets, offset_transform): + master_transform, paths, transforms, offsets, offset_transform +): r""" Get bounding box of a `.PathCollection`\s internal objects. @@ -1123,11 +1172,12 @@ def get_path_collection_extents( - (C, α, O) """ from .transforms import Bbox + if len(paths) == 0: raise ValueError("No paths provided") if len(offsets) == 0: raise ValueError("No offsets provided") extents, minpos = _path.get_path_collection_extents( - master_transform, paths, np.atleast_3d(transforms), - offsets, offset_transform) + master_transform, paths, np.atleast_3d(transforms), offsets, offset_transform + ) return Bbox.from_extents(*extents, minpos=minpos) diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index a61f01c0d48a..bef1c5f8184f 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -19,8 +19,7 @@ def test_empty_closed_path(): path = Path(np.zeros((0, 2)), closed=True) assert path.vertices.shape == (0, 2) assert path.codes is None - assert_array_equal(path.get_extents().extents, - transforms.Bbox.null().extents) + assert_array_equal(path.get_extents().extents, transforms.Bbox.null().extents) def test_readonly_path(): @@ -35,19 +34,19 @@ def modify_vertices(): def test_path_exceptions(): bad_verts1 = np.arange(12).reshape(4, 3) - with pytest.raises(ValueError, - match=re.escape(f'has shape {bad_verts1.shape}')): + with pytest.raises(ValueError, match=re.escape(f"has shape {bad_verts1.shape}")): Path(bad_verts1) bad_verts2 = np.arange(12).reshape(2, 3, 2) - with pytest.raises(ValueError, - match=re.escape(f'has shape {bad_verts2.shape}')): + with pytest.raises(ValueError, match=re.escape(f"has shape {bad_verts2.shape}")): Path(bad_verts2) good_verts = np.arange(12).reshape(6, 2) bad_codes = np.arange(2) - msg = re.escape(f"Your vertices have shape {good_verts.shape} " - f"but your codes have shape {bad_codes.shape}") + msg = re.escape( + f"Your vertices have shape {good_verts.shape} " + f"but your codes have shape {bad_codes.shape}" + ) with pytest.raises(ValueError, match=msg): Path(good_verts, bad_codes) @@ -57,23 +56,62 @@ def test_point_in_path(): path = Path._create_closed([(0, 0), (0, 1), (1, 1), (1, 0)]) points = [(0.5, 0.5), (1.5, 0.5)] ret = path.contains_points(points) - assert ret.dtype == 'bool' + assert ret.dtype == "bool" np.testing.assert_equal(ret, [True, False]) @pytest.mark.parametrize( "other_path, inside, inverted_inside", - [(Path([(0.25, 0.25), (0.25, 0.75), (0.75, 0.75), (0.75, 0.25), (0.25, 0.25)], - closed=True), True, False), - (Path([(-0.25, -0.25), (-0.25, 1.75), (1.75, 1.75), (1.75, -0.25), (-0.25, -0.25)], - closed=True), False, True), - (Path([(-0.25, -0.25), (-0.25, 1.75), (0.5, 0.5), - (1.75, 1.75), (1.75, -0.25), (-0.25, -0.25)], - closed=True), False, False), - (Path([(0.25, 0.25), (0.25, 1.25), (1.25, 1.25), (1.25, 0.25), (0.25, 0.25)], - closed=True), False, False), - (Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True), False, False), - (Path([(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)], closed=True), False, False)]) + [ + ( + Path( + [(0.25, 0.25), (0.25, 0.75), (0.75, 0.75), (0.75, 0.25), (0.25, 0.25)], + closed=True, + ), + True, + False, + ), + ( + Path( + [ + (-0.25, -0.25), + (-0.25, 1.75), + (1.75, 1.75), + (1.75, -0.25), + (-0.25, -0.25), + ], + closed=True, + ), + False, + True, + ), + ( + Path( + [ + (-0.25, -0.25), + (-0.25, 1.75), + (0.5, 0.5), + (1.75, 1.75), + (1.75, -0.25), + (-0.25, -0.25), + ], + closed=True, + ), + False, + False, + ), + ( + Path( + [(0.25, 0.25), (0.25, 1.25), (1.25, 1.25), (1.25, 0.25), (0.25, 0.25)], + closed=True, + ), + False, + False, + ), + (Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True), False, False), + (Path([(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)], closed=True), False, False), + ], +) def test_contains_path(other_path, inside, inverted_inside): path = Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True) assert path.contains_path(other_path) is inside @@ -90,8 +128,10 @@ def test_contains_points_negative_radius(): _test_paths = [ # interior extrema determine extents and degenerate derivative - Path([[0, 0], [1, 0], [1, 1], [0, 1]], - [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]), + Path( + [[0, 0], [1, 0], [1, 1], [0, 1]], + [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4], + ), # a quadratic curve Path([[0, 0], [0, 1], [1, 0]], [Path.MOVETO, Path.CURVE3, Path.CURVE3]), # a linear curve, degenerate vertically @@ -101,11 +141,15 @@ def test_contains_points_negative_radius(): ] -_test_path_extents = [(0., 0., 0.75, 1.), (0., 0., 1., 0.5), (0., 1., 1., 1.), - (1., 2., 1., 2.)] +_test_path_extents = [ + (0.0, 0.0, 0.75, 1.0), + (0.0, 0.0, 1.0, 0.5), + (0.0, 1.0, 1.0, 1.0), + (1.0, 2.0, 1.0, 2.0), +] -@pytest.mark.parametrize('path, extents', zip(_test_paths, _test_path_extents)) +@pytest.mark.parametrize("path, extents", list(zip(_test_paths, _test_path_extents))) def test_exact_extents(path, extents): # notice that if we just looked at the control points to get the bounding # box of each curve, we would get the wrong answers. For example, for @@ -119,14 +163,12 @@ def test_exact_extents(path, extents): assert np.all(path.get_extents().extents == extents) -@pytest.mark.parametrize('ignored_code', [Path.CLOSEPOLY, Path.STOP]) +@pytest.mark.parametrize("ignored_code", [Path.CLOSEPOLY, Path.STOP]) def test_extents_with_ignored_codes(ignored_code): # Check that STOP and CLOSEPOLY points are ignored when calculating extents # of a path with only straight lines - path = Path([[0, 0], - [1, 1], - [2, 2]], [Path.MOVETO, Path.MOVETO, ignored_code]) - assert np.all(path.get_extents().extents == (0., 0., 1., 1.)) + path = Path([[0, 0], [1, 1], [2, 2]], [Path.MOVETO, Path.MOVETO, ignored_code]) + assert np.all(path.get_extents().extents == (0.0, 0.0, 1.0, 1.0)) def test_point_in_path_nan(): @@ -143,15 +185,22 @@ def test_nonlinear_containment(): ax.set(xscale="log", ylim=(0, 1)) polygon = ax.axvspan(1, 10) assert polygon.get_path().contains_point( - ax.transData.transform((5, .5)), polygon.get_transform()) + ax.transData.transform((5, 0.5)), polygon.get_transform() + ) assert not polygon.get_path().contains_point( - ax.transData.transform((.5, .5)), polygon.get_transform()) + ax.transData.transform((0.5, 0.5)), polygon.get_transform() + ) assert not polygon.get_path().contains_point( - ax.transData.transform((50, .5)), polygon.get_transform()) + ax.transData.transform((50, 0.5)), polygon.get_transform() + ) -@image_comparison(['arrow_contains_point.png'], remove_text=True, style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.027) +@image_comparison( + ["arrow_contains_point.png"], + remove_text=True, + style="mpl20", + tol=0 if platform.machine() == "x86_64" else 0.027, +) def test_arrow_contains_point(): # fix bug (#8384) fig, ax = plt.subplots() @@ -159,29 +208,27 @@ def test_arrow_contains_point(): ax.set_ylim(0, 2) # create an arrow with Curve style - arrow = patches.FancyArrowPatch((0.5, 0.25), (1.5, 0.75), - arrowstyle='->', - mutation_scale=40) + arrow = patches.FancyArrowPatch( + (0.5, 0.25), (1.5, 0.75), arrowstyle="->", mutation_scale=40 + ) ax.add_patch(arrow) # create an arrow with Bracket style - arrow1 = patches.FancyArrowPatch((0.5, 1), (1.5, 1.25), - arrowstyle=']-[', - mutation_scale=40) + arrow1 = patches.FancyArrowPatch( + (0.5, 1), (1.5, 1.25), arrowstyle="]-[", mutation_scale=40 + ) ax.add_patch(arrow1) # create an arrow with other arrow style - arrow2 = patches.FancyArrowPatch((0.5, 1.5), (1.5, 1.75), - arrowstyle='fancy', - fill=False, - mutation_scale=40) + arrow2 = patches.FancyArrowPatch( + (0.5, 1.5), (1.5, 1.75), arrowstyle="fancy", fill=False, mutation_scale=40 + ) ax.add_patch(arrow2) patches_list = [arrow, arrow1, arrow2] # generate some points - X, Y = np.meshgrid(np.arange(0, 2, 0.1), - np.arange(0, 2, 0.1)) + X, Y = np.meshgrid(np.arange(0, 2, 0.1), np.arange(0, 2, 0.1)) for k, (x, y) in enumerate(zip(X.ravel(), Y.ravel())): xdisp, ydisp = ax.transData.transform([x, y]) - event = MouseEvent('button_press_event', fig.canvas, xdisp, ydisp) + event = MouseEvent("button_press_event", fig.canvas, xdisp, ydisp) for m, patch in enumerate(patches_list): # set the points to red only if the arrow contains the point inside, res = patch.contains(event) @@ -189,33 +236,35 @@ def test_arrow_contains_point(): ax.scatter(x, y, s=5, c="r") -@image_comparison(['path_clipping.svg'], remove_text=True) +@image_comparison(["path_clipping.svg"], remove_text=True) def test_path_clipping(): fig = plt.figure(figsize=(6.0, 6.2)) - for i, xy in enumerate([ + for i, xy in enumerate( + [ [(200, 200), (200, 350), (400, 350), (400, 200)], [(200, 200), (200, 350), (400, 350), (400, 100)], [(200, 100), (200, 350), (400, 350), (400, 100)], [(200, 100), (200, 415), (400, 350), (400, 100)], [(200, 100), (200, 415), (400, 415), (400, 100)], [(200, 415), (400, 415), (400, 100), (200, 100)], - [(400, 415), (400, 100), (200, 100), (200, 415)]]): - ax = fig.add_subplot(4, 2, i+1) + [(400, 415), (400, 100), (200, 100), (200, 415)], + ] + ): + ax = fig.add_subplot(4, 2, i + 1) bbox = [0, 140, 640, 260] ax.set_xlim(bbox[0], bbox[0] + bbox[2]) ax.set_ylim(bbox[1], bbox[1] + bbox[3]) - ax.add_patch(Polygon( - xy, facecolor='none', edgecolor='red', closed=True)) + ax.add_patch(Polygon(xy, facecolor="none", edgecolor="red", closed=True)) -@image_comparison(['semi_log_with_zero.png'], style='mpl20') +@image_comparison(["semi_log_with_zero.png"], style="mpl20") def test_log_transform_with_zero(): x = np.arange(-10, 10) - y = (1.0 - 1.0/(x**2+1))**20 + y = (1.0 - 1.0 / (x**2 + 1)) ** 20 fig, ax = plt.subplots() - ax.semilogy(x, y, "-o", lw=15, markeredgecolor='k') + ax.semilogy(x, y, "-o", lw=15, markeredgecolor="k") ax.set_ylim(1e-7, 1) ax.grid(True) @@ -235,14 +284,14 @@ def test_make_compound_path_empty(): def test_make_compound_path_stops(): zero = [0, 0] - paths = 3*[Path([zero, zero], [Path.MOVETO, Path.STOP])] + paths = 3 * [Path([zero, zero], [Path.MOVETO, Path.STOP])] compound_path = Path.make_compound_path(*paths) # the choice to not preserve the terminal STOP is arbitrary, but # documented, so we test that it is in fact respected here assert np.sum(compound_path.codes == Path.STOP) == 0 -@image_comparison(['xkcd.png'], remove_text=True) +@image_comparison(["xkcd.png"], remove_text=True) def test_xkcd(): np.random.seed(0) @@ -254,7 +303,7 @@ def test_xkcd(): ax.plot(x, y) -@image_comparison(['xkcd_marker.png'], remove_text=True) +@image_comparison(["xkcd_marker.png"], remove_text=True) def test_xkcd_marker(): np.random.seed(0) @@ -265,25 +314,27 @@ def test_xkcd_marker(): with plt.xkcd(): fig, ax = plt.subplots() - ax.plot(x, y1, '+', ms=10) - ax.plot(x, y2, 'o', ms=10) - ax.plot(x, y3, '^', ms=10) + ax.plot(x, y1, "+", ms=10) + ax.plot(x, y2, "o", ms=10) + ax.plot(x, y3, "^", ms=10) -@image_comparison(['marker_paths.pdf'], remove_text=True) +@image_comparison(["marker_paths.pdf"], remove_text=True) def test_marker_paths_pdf(): N = 7 - plt.errorbar(np.arange(N), - np.ones(N) + 4, - np.ones(N)) + plt.errorbar(np.arange(N), np.ones(N) + 4, np.ones(N)) plt.xlim(-1, N) plt.ylim(-1, 7) -@image_comparison(['nan_path'], style='default', remove_text=True, - extensions=['pdf', 'svg', 'eps', 'png'], - tol=0 if platform.machine() == 'x86_64' else 0.009) +@image_comparison( + ["nan_path"], + style="default", + remove_text=True, + extensions=["pdf", "svg", "eps", "png"], + tol=0 if platform.machine() == "x86_64" else 0.009, +) def test_nan_isolated_points(): y0 = [0, np.nan, 2, np.nan, 4, 5, 6] @@ -291,34 +342,37 @@ def test_nan_isolated_points(): fig, ax = plt.subplots() - ax.plot(y0, '-o') - ax.plot(y1, '-o') + ax.plot(y0, "-o") + ax.plot(y1, "-o") def test_path_no_doubled_point_in_to_polygon(): hand = np.array( - [[1.64516129, 1.16145833], - [1.64516129, 1.59375], - [1.35080645, 1.921875], - [1.375, 2.18229167], - [1.68548387, 1.9375], - [1.60887097, 2.55208333], - [1.68548387, 2.69791667], - [1.76209677, 2.56770833], - [1.83064516, 1.97395833], - [1.89516129, 2.75], - [1.9516129, 2.84895833], - [2.01209677, 2.76041667], - [1.99193548, 1.99479167], - [2.11290323, 2.63020833], - [2.2016129, 2.734375], - [2.25403226, 2.60416667], - [2.14919355, 1.953125], - [2.30645161, 2.36979167], - [2.39112903, 2.36979167], - [2.41532258, 2.1875], - [2.1733871, 1.703125], - [2.07782258, 1.16666667]]) + [ + [1.64516129, 1.16145833], + [1.64516129, 1.59375], + [1.35080645, 1.921875], + [1.375, 2.18229167], + [1.68548387, 1.9375], + [1.60887097, 2.55208333], + [1.68548387, 2.69791667], + [1.76209677, 2.56770833], + [1.83064516, 1.97395833], + [1.89516129, 2.75], + [1.9516129, 2.84895833], + [2.01209677, 2.76041667], + [1.99193548, 1.99479167], + [2.11290323, 2.63020833], + [2.2016129, 2.734375], + [2.25403226, 2.60416667], + [2.14919355, 1.953125], + [2.30645161, 2.36979167], + [2.39112903, 2.36979167], + [2.41532258, 2.1875], + [2.1733871, 1.703125], + [2.07782258, 1.16666667], + ] + ) (r0, c0, r1, c1) = (1.0, 1.5, 2.1, 2.5) @@ -335,8 +389,7 @@ def test_path_to_polygons(): p = Path(data) assert_array_equal(p.to_polygons(width=40, height=40), []) - assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), - [data]) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) assert_array_equal(p.to_polygons(), []) assert_array_equal(p.to_polygons(closed_only=False), [data]) @@ -345,8 +398,7 @@ def test_path_to_polygons(): p = Path(data) assert_array_equal(p.to_polygons(width=40, height=40), [closed_data]) - assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), - [data]) + assert_array_equal(p.to_polygons(width=40, height=40, closed_only=False), [data]) assert_array_equal(p.to_polygons(), [closed_data]) assert_array_equal(p.to_polygons(closed_only=False), [data]) @@ -415,9 +467,15 @@ def test_path_shallowcopy(): assert path2.codes is path2_copy.codes -@pytest.mark.parametrize('phi', np.concatenate([ - np.array([0, 15, 30, 45, 60, 75, 90, 105, 120, 135]) + delta - for delta in [-1, 0, 1]])) +@pytest.mark.parametrize( + "phi", + np.concatenate( + [ + np.array([0, 15, 30, 45, 60, 75, 90, 105, 120, 135]) + delta + for delta in [-1, 0, 1] + ] + ), +) def test_path_intersect_path(phi): # test for the range of intersection angles eps_array = [1e-5, 1e-8, 1e-10, 1e-12] @@ -494,12 +552,12 @@ def test_path_intersect_path(phi): assert not a.intersects_path(b) and not b.intersects_path(a) # a and b are collinear but do not intersect - a = transform.transform_path(Path([(0., -5.), (1., -5.)])) - b = transform.transform_path(Path([(1., 5.), (0., 5.)])) + a = transform.transform_path(Path([(0.0, -5.0), (1.0, -5.0)])) + b = transform.transform_path(Path([(1.0, 5.0), (0.0, 5.0)])) assert not a.intersects_path(b) and not b.intersects_path(a) -@pytest.mark.parametrize('offset', range(-720, 361, 45)) +@pytest.mark.parametrize("offset", range(-720, 361, 45)) def test_full_arc(offset): low = offset high = 360 + offset @@ -513,23 +571,30 @@ def test_full_arc(offset): def test_disjoint_zero_length_segment(): this_path = Path( - np.array([ - [824.85064295, 2056.26489203], - [861.69033931, 2041.00539016], - [868.57864109, 2057.63522175], - [831.73894473, 2072.89472361], - [824.85064295, 2056.26489203]]), - np.array([1, 2, 2, 2, 79], dtype=Path.code_type)) + np.array( + [ + [824.85064295, 2056.26489203], + [861.69033931, 2041.00539016], + [868.57864109, 2057.63522175], + [831.73894473, 2072.89472361], + [824.85064295, 2056.26489203], + ] + ), + np.array([1, 2, 2, 2, 79], dtype=Path.code_type), + ) outline_path = Path( - np.array([ - [859.91051028, 2165.38461538], - [859.06772495, 2149.30331334], - [859.06772495, 2181.46591743], - [859.91051028, 2165.38461538], - [859.91051028, 2165.38461538]]), - np.array([1, 2, 2, 2, 2], - dtype=Path.code_type)) + np.array( + [ + [859.91051028, 2165.38461538], + [859.06772495, 2149.30331334], + [859.06772495, 2181.46591743], + [859.91051028, 2165.38461538], + [859.91051028, 2165.38461538], + ] + ), + np.array([1, 2, 2, 2, 2], dtype=Path.code_type), + ) assert not outline_path.intersects_path(this_path) assert not this_path.intersects_path(outline_path) @@ -537,18 +602,24 @@ def test_disjoint_zero_length_segment(): def test_intersect_zero_length_segment(): this_path = Path( - np.array([ - [0, 0], - [1, 1], - ])) + np.array( + [ + [0, 0], + [1, 1], + ] + ) + ) outline_path = Path( - np.array([ - [1, 0], - [.5, .5], - [.5, .5], - [0, 1], - ])) + np.array( + [ + [1, 0], + [0.5, 0.5], + [0.5, 0.5], + [0, 1], + ] + ) + ) assert outline_path.intersects_path(this_path) assert this_path.intersects_path(outline_path) @@ -560,16 +631,16 @@ def test_cleanup_closepoly(): # control points but also the CLOSEPOLY, since it has nowhere valid to # point. paths = [ - Path([[np.nan, np.nan], [np.nan, np.nan]], - [Path.MOVETO, Path.CLOSEPOLY]), + Path([[np.nan, np.nan], [np.nan, np.nan]], [Path.MOVETO, Path.CLOSEPOLY]), # we trigger a different path in the C++ code if we don't pass any # codes explicitly, so we must also make sure that this works Path([[np.nan, np.nan], [np.nan, np.nan]]), # we should also make sure that this cleanup works if there's some # multi-vertex curves - Path([[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan], - [np.nan, np.nan]], - [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY]) + Path( + [[np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]], + [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY], + ), ] for p in paths: cleaned = p.cleaned(remove_nans=True) @@ -579,12 +650,7 @@ def test_cleanup_closepoly(): def test_interpolated_moveto(): # Initial path has two subpaths with two LINETOs each - vertices = np.array([[0, 0], - [0, 1], - [1, 2], - [4, 4], - [4, 5], - [5, 5]]) + vertices = np.array([[0, 0], [0, 1], [1, 2], [4, 4], [4, 5], [5, 5]]) codes = [Path.MOVETO, Path.LINETO, Path.LINETO] * 2 path = Path(vertices, codes) @@ -596,20 +662,16 @@ def test_interpolated_moveto(): def test_interpolated_closepoly(): - codes = [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY] + codes = [Path.MOVETO] + [Path.LINETO] * 2 + [Path.CLOSEPOLY] vertices = [(4, 3), (5, 4), (5, 3), (0, 0)] path = Path(vertices, codes) result = path.interpolated(2) - expected_vertices = np.array([[4, 3], - [4.5, 3.5], - [5, 4], - [5, 3.5], - [5, 3], - [4.5, 3], - [4, 3]]) - expected_codes = [Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY] + expected_vertices = np.array( + [[4, 3], [4.5, 3.5], [5, 4], [5, 3.5], [5, 3], [4.5, 3], [4, 3]] + ) + expected_codes = [Path.MOVETO] + [Path.LINETO] * 5 + [Path.CLOSEPOLY] np.testing.assert_allclose(result.vertices, expected_vertices) np.testing.assert_array_equal(result.codes, expected_codes) @@ -621,8 +683,7 @@ def test_interpolated_closepoly(): path = Path(vertices, codes) result = path.interpolated(2) - extra_expected_vertices = np.array([[3, 2], - [2, 1]]) + extra_expected_vertices = np.array([[3, 2], [2, 1]]) expected_vertices = np.concatenate([expected_vertices, extra_expected_vertices]) expected_codes += [Path.LINETO] * 2 @@ -633,21 +694,17 @@ def test_interpolated_closepoly(): def test_interpolated_moveto_closepoly(): # Initial path has two closed subpaths - codes = ([Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]) * 2 + codes = ([Path.MOVETO] + [Path.LINETO] * 2 + [Path.CLOSEPOLY]) * 2 vertices = [(4, 3), (5, 4), (5, 3), (0, 0), (8, 6), (10, 8), (10, 6), (0, 0)] path = Path(vertices, codes) result = path.interpolated(2) - expected_vertices1 = np.array([[4, 3], - [4.5, 3.5], - [5, 4], - [5, 3.5], - [5, 3], - [4.5, 3], - [4, 3]]) + expected_vertices1 = np.array( + [[4, 3], [4.5, 3.5], [5, 4], [5, 3.5], [5, 3], [4.5, 3], [4, 3]] + ) expected_vertices = np.concatenate([expected_vertices1, expected_vertices1 * 2]) - expected_codes = ([Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY]) * 2 + expected_codes = ([Path.MOVETO] + [Path.LINETO] * 5 + [Path.CLOSEPOLY]) * 2 np.testing.assert_allclose(result.vertices, expected_vertices) np.testing.assert_array_equal(result.codes, expected_codes) From c4bf93df9c0b231a26d3ba13e536e61be9385585 Mon Sep 17 00:00:00 2001 From: DavidAG Date: Wed, 17 Dec 2025 18:56:17 +0100 Subject: [PATCH 2/3] Fix CI: Revert path.py and optimize PolyCollection closing safely --- lib/matplotlib/collections.py | 727 +++++++++++++++++++++------------- lib/matplotlib/path.py | 7 +- 2 files changed, 454 insertions(+), 280 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 684e15cdf854..47fdb3d7f435 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -18,21 +18,34 @@ import numpy as np import matplotlib as mpl -from . import (_api, _path, artist, cbook, colorizer as mcolorizer, colors as mcolors, - _docstring, hatch as mhatch, lines as mlines, path as mpath, transforms) +from . import ( + _api, + _path, + artist, + cbook, + colorizer as mcolorizer, + colors as mcolors, + _docstring, + hatch as mhatch, + lines as mlines, + path as mpath, + transforms, +) from ._enums import JoinStyle, CapStyle # "color" is excluded; it is a compound setter, and its docstring differs # in LineCollection. -@_api.define_aliases({ - "antialiased": ["antialiaseds", "aa"], - "edgecolor": ["edgecolors", "ec"], - "facecolor": ["facecolors", "fc"], - "linestyle": ["linestyles", "dashes", "ls"], - "linewidth": ["linewidths", "lw"], - "offset_transform": ["transOffset"], -}) +@_api.define_aliases( + { + "antialiased": ["antialiaseds", "aa"], + "edgecolor": ["edgecolors", "ec"], + "facecolor": ["facecolors", "fc"], + "linestyle": ["linestyles", "dashes", "ls"], + "linewidth": ["linewidths", "lw"], + "offset_transform": ["transOffset"], + } +) class Collection(mcolorizer.ColorizingArtist): r""" Base class for Collections. Must be subclassed to be usable. @@ -63,6 +76,7 @@ class Collection(mcolorizer.ColorizingArtist): mappable will be used to set the ``facecolors`` and ``edgecolors``, ignoring those that were manually passed in. """ + #: Either a list of 3x3 arrays or an Nx3x3 array (representing N #: transforms), suitable for the `all_transforms` argument to #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`; @@ -76,26 +90,28 @@ class Collection(mcolorizer.ColorizingArtist): _edge_default = False @_docstring.interpd - def __init__(self, *, - edgecolors=None, - facecolors=None, - hatchcolors=None, - linewidths=None, - linestyles='solid', - capstyle=None, - joinstyle=None, - antialiaseds=None, - offsets=None, - offset_transform=None, - norm=None, # optional for ScalarMappable - cmap=None, # ditto - colorizer=None, - pickradius=5.0, - hatch=None, - urls=None, - zorder=1, - **kwargs - ): + def __init__( + self, + *, + edgecolors=None, + facecolors=None, + hatchcolors=None, + linewidths=None, + linestyles="solid", + capstyle=None, + joinstyle=None, + antialiaseds=None, + offsets=None, + offset_transform=None, + norm=None, # optional for ScalarMappable + cmap=None, # ditto + colorizer=None, + pickradius=5.0, + hatch=None, + urls=None, + zorder=1, + **kwargs, + ): """ Parameters ---------- @@ -179,7 +195,7 @@ def __init__(self, *, self._face_is_mapped = None self._edge_is_mapped = None self._mapped_colors = None # calculated in update_scalarmappable - self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] + self._hatch_linewidth = mpl.rcParams["hatch.linewidth"] self.set_facecolor(facecolors) self.set_edgecolor(edgecolors) self.set_linewidth(linewidths) @@ -228,10 +244,10 @@ def get_offset_transform(self): """Return the `.Transform` instance used by this artist offset.""" if self._offset_transform is None: self._offset_transform = transforms.IdentityTransform() - elif (not isinstance(self._offset_transform, transforms.Transform) - and hasattr(self._offset_transform, '_as_mpl_transform')): - self._offset_transform = \ - self._offset_transform._as_mpl_transform(self.axes) + elif not isinstance(self._offset_transform, transforms.Transform) and hasattr( + self._offset_transform, "_as_mpl_transform" + ): + self._offset_transform = self._offset_transform._as_mpl_transform(self.axes) return self._offset_transform def set_offset_transform(self, offset_transform): @@ -263,8 +279,10 @@ def get_datalim(self, transData): transform = self.get_transform() offset_trf = self.get_offset_transform() - if not (isinstance(offset_trf, transforms.IdentityTransform) - or offset_trf.contains_branch(transData)): + if not ( + isinstance(offset_trf, transforms.IdentityTransform) + or offset_trf.contains_branch(transData) + ): # if the offsets are in some coords other than data, # then don't use them for autoscaling. return transforms.Bbox.null() @@ -292,10 +310,12 @@ def get_datalim(self, transData): offsets = offsets.filled(np.nan) # get_path_collection_extents handles nan but not masked arrays return mpath.get_path_collection_extents( - transform.get_affine() - transData, paths, + transform.get_affine() - transData, + paths, self.get_transforms(), offset_trf.transform_non_affine(offsets), - offset_trf.get_affine().frozen()) + offset_trf.get_affine().frozen(), + ) # NOTE: None is the default case where no offsets were passed in if self._offsets is not None: @@ -339,8 +359,7 @@ def _prepare_points(self): offsets = np.ma.column_stack([xs, ys]) if not transform.is_affine: - paths = [transform.transform_path_non_affine(path) - for path in paths] + paths = [transform.transform_path_non_affine(path) for path in paths] transform = transform.get_affine() if not offset_trf.is_affine: offsets = offset_trf.transform_non_affine(offsets) @@ -377,6 +396,7 @@ def draw(self, renderer): if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer + renderer = PathEffectRenderer(self.get_path_effects(), renderer) # If the collection is made up of a single shape/color/stroke, @@ -389,19 +409,26 @@ def draw(self, renderer): facecolors = self.get_facecolor() edgecolors = self.get_edgecolor() do_single_path_optimization = False - if (len(paths) == 1 and len(trans) <= 1 and - len(facecolors) == 1 and len(edgecolors) == 1 and - len(self._linewidths) == 1 and - all(ls[1] is None for ls in self._linestyles) and - len(self._antialiaseds) == 1 and len(self._urls) == 1 and - self.get_hatch() is None): + if ( + len(paths) == 1 + and len(trans) <= 1 + and len(facecolors) == 1 + and len(edgecolors) == 1 + and len(self._linewidths) == 1 + and all(ls[1] is None for ls in self._linestyles) + and len(self._antialiaseds) == 1 + and len(self._urls) == 1 + and self.get_hatch() is None + ): if len(trans): combined_transform = transforms.Affine2D(trans[0]) + transform else: combined_transform = transform extents = paths[0].get_extents(combined_transform) - if (extents.width < self.get_figure(root=True).bbox.width - and extents.height < self.get_figure(root=True).bbox.height): + if ( + extents.width < self.get_figure(root=True).bbox.width + and extents.height < self.get_figure(root=True).bbox.height + ): do_single_path_optimization = True if self._joinstyle: @@ -417,8 +444,13 @@ def draw(self, renderer): gc.set_antialiased(self._antialiaseds[0]) gc.set_url(self._urls[0]) renderer.draw_markers( - gc, paths[0], combined_transform.frozen(), - mpath.Path(offsets), offset_trf, tuple(facecolors[0])) + gc, + paths[0], + combined_transform.frozen(), + mpath.Path(offsets), + offset_trf, + tuple(facecolors[0]), + ) else: # The current new API of draw_path_collection() is provisional # and will be changed in a future PR. @@ -430,12 +462,20 @@ def draw(self, renderer): hatchcolors_arg_supported = True try: renderer.draw_path_collection( - gc, transform.frozen(), [], - self.get_transforms(), offsets, offset_trf, - self.get_facecolor(), self.get_edgecolor(), - self._linewidths, self._linestyles, - self._antialiaseds, self._urls, - "screen", hatchcolors=self.get_hatchcolor() + gc, + transform.frozen(), + [], + self.get_transforms(), + offsets, + offset_trf, + self.get_facecolor(), + self.get_edgecolor(), + self._linewidths, + self._linestyles, + self._antialiaseds, + self._urls, + "screen", + hatchcolors=self.get_hatchcolor(), ) except TypeError: # If the renderer does not support the hatchcolors argument, @@ -446,29 +486,47 @@ def draw(self, renderer): # If the hatchcolors argument is not needed or not passed # then we can skip the iteration over paths in case the # argument is not supported by the renderer. - hatchcolors_not_needed = (self.get_hatch() is None or - self._original_hatchcolor is None) + hatchcolors_not_needed = ( + self.get_hatch() is None or self._original_hatchcolor is None + ) if self._gapcolor is not None: # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() - args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, - self._linewidths, ilinestyles, self._antialiaseds, self._urls, - "screen"] + args = [ + offsets, + offset_trf, + [mcolors.to_rgba("none")], + self._gapcolor, + self._linewidths, + ilinestyles, + self._antialiaseds, + self._urls, + "screen", + ] if hatchcolors_arg_supported: - renderer.draw_path_collection(gc, transform.frozen(), ipaths, - self.get_transforms(), *args, - hatchcolors=self.get_hatchcolor()) + renderer.draw_path_collection( + gc, + transform.frozen(), + ipaths, + self.get_transforms(), + *args, + hatchcolors=self.get_hatchcolor(), + ) else: if hatchcolors_not_needed: - renderer.draw_path_collection(gc, transform.frozen(), ipaths, - self.get_transforms(), *args) + renderer.draw_path_collection( + gc, transform.frozen(), ipaths, self.get_transforms(), *args + ) else: path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), ipaths, self.get_transforms()) + transform.frozen(), ipaths, self.get_transforms() + ) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, + gc, + list(path_ids), + *args, hatchcolors=self.get_hatchcolor(), ): path, transform = path_id @@ -477,23 +535,41 @@ def draw(self, renderer): transform.translate(xo, yo) renderer.draw_path(gc0, path, transform, rgbFace) - args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), - self._linewidths, self._linestyles, self._antialiaseds, self._urls, - "screen"] + args = [ + offsets, + offset_trf, + self.get_facecolor(), + self.get_edgecolor(), + self._linewidths, + self._linestyles, + self._antialiaseds, + self._urls, + "screen", + ] if hatchcolors_arg_supported: - renderer.draw_path_collection(gc, transform.frozen(), paths, - self.get_transforms(), *args, - hatchcolors=self.get_hatchcolor()) + renderer.draw_path_collection( + gc, + transform.frozen(), + paths, + self.get_transforms(), + *args, + hatchcolors=self.get_hatchcolor(), + ) else: if hatchcolors_not_needed: - renderer.draw_path_collection(gc, transform.frozen(), paths, - self.get_transforms(), *args) + renderer.draw_path_collection( + gc, transform.frozen(), paths, self.get_transforms(), *args + ) else: path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), paths, self.get_transforms()) + transform.frozen(), paths, self.get_transforms() + ) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), + gc, + list(path_ids), + *args, + hatchcolors=self.get_hatchcolor(), ): path, transform = path_id if xo != 0 or yo != 0: @@ -516,7 +592,8 @@ def set_pickradius(self, pickradius): """ if not isinstance(pickradius, Real): raise ValueError( - f"pickradius must be a real-valued number, not {pickradius!r}") + f"pickradius must be a real-valued number, not {pickradius!r}" + ) self._pickradius = pickradius def get_pickradius(self): @@ -533,9 +610,10 @@ def contains(self, mouseevent): return False, {} pickradius = ( float(self._picker) - if isinstance(self._picker, Number) and - self._picker is not True # the bool, not just nonzero or 1 - else self._pickradius) + if isinstance(self._picker, Number) + and self._picker is not True # the bool, not just nonzero or 1 + else self._pickradius + ) if self.axes: self.axes._unstale_viewLim() transform, offset_trf, offsets, paths = self._prepare_points() @@ -545,9 +623,16 @@ def contains(self, mouseevent): # following the path. If pickradius <= 0, then we instead simply check # if the point is *inside* of the path instead. ind = _path.point_in_path_collection( - mouseevent.x, mouseevent.y, pickradius, - transform.frozen(), paths, self.get_transforms(), - offsets, offset_trf, pickradius <= 0) + mouseevent.x, + mouseevent.y, + pickradius, + transform.frozen(), + paths, + self.get_transforms(), + offsets, + offset_trf, + pickradius <= 0, + ) return len(ind) > 0, dict(ind=ind) def set_urls(self, urls): @@ -630,11 +715,17 @@ def set_offsets(self, offsets): offsets = np.asanyarray(offsets) if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else. offsets = offsets[None, :] - cstack = (np.ma.column_stack if isinstance(offsets, np.ma.MaskedArray) - else np.column_stack) + cstack = ( + np.ma.column_stack + if isinstance(offsets, np.ma.MaskedArray) + else np.column_stack + ) self._offsets = cstack( - (np.asanyarray(self.convert_xunits(offsets[:, 0]), float), - np.asanyarray(self.convert_yunits(offsets[:, 1]), float))) + ( + np.asanyarray(self.convert_xunits(offsets[:, 0]), float), + np.asanyarray(self.convert_yunits(offsets[:, 1]), float), + ) + ) self.stale = True def get_offsets(self): @@ -644,7 +735,7 @@ def get_offsets(self): def _get_default_linewidth(self): # This may be overridden in a subclass. - return mpl.rcParams['patch.linewidth'] # validated as float + return mpl.rcParams["patch.linewidth"] # validated as float def set_linewidth(self, lw): """ @@ -663,7 +754,8 @@ def set_linewidth(self, lw): # scale all of the dash patterns. self._linewidths, self._linestyles = self._bcast_lwls( - self._us_lw, self._us_linestyles) + self._us_lw, self._us_linestyles + ) self.stale = True def set_linestyle(self, ls): @@ -697,7 +789,8 @@ def set_linestyle(self, ls): # broadcast and scale the lw and dash patterns self._linewidths, self._linestyles = self._bcast_lwls( - self._us_lw, self._us_linestyles) + self._us_lw, self._us_linestyles + ) @_docstring.interpd def set_capstyle(self, cs): @@ -765,7 +858,7 @@ def _bcast_lwls(linewidths, dashes): linewidths, dashes : list Will be the same length, dashes are scaled by paired linewidth """ - if mpl.rcParams['_internal.classic_mode']: + if mpl.rcParams["_internal.classic_mode"]: return linewidths, dashes # make sure they are the same length so we can zip them if len(dashes) != len(linewidths): @@ -776,8 +869,9 @@ def _bcast_lwls(linewidths, dashes): linewidths = list(linewidths) * (l_dashes // gcd) # scale the dash patterns - dashes = [mlines._scale_dashes(o, d, lw) - for (o, d), lw in zip(dashes, linewidths)] + dashes = [ + mlines._scale_dashes(o, d, lw) for (o, d), lw in zip(dashes, linewidths) + ] return linewidths, dashes @@ -806,7 +900,7 @@ def set_antialiased(self, aa): def _get_default_antialiased(self): # This may be overridden in a subclass. - return mpl.rcParams['patch.antialiased'] + return mpl.rcParams["patch.antialiased"] def set_color(self, c): """ @@ -830,7 +924,7 @@ def set_color(self, c): def _get_default_facecolor(self): # This may be overridden in a subclass. - return mpl.rcParams['patch.facecolor'] + return mpl.rcParams["patch.facecolor"] def _set_facecolor(self, c): if c is None: @@ -860,33 +954,36 @@ def get_facecolor(self): return self._facecolors def get_edgecolor(self): - if cbook._str_equal(self._edgecolors, 'face'): + if cbook._str_equal(self._edgecolors, "face"): return self.get_facecolor() else: return self._edgecolors def _get_default_edgecolor(self): # This may be overridden in a subclass. - return mpl.rcParams['patch.edgecolor'] + return mpl.rcParams["patch.edgecolor"] def get_hatchcolor(self): - if cbook._str_equal(self._hatchcolors, 'edge'): + if cbook._str_equal(self._hatchcolors, "edge"): if len(self.get_edgecolor()) == 0: - return mpl.colors.to_rgba_array(self._get_default_edgecolor(), - self._alpha) + return mpl.colors.to_rgba_array( + self._get_default_edgecolor(), self._alpha + ) return self.get_edgecolor() return self._hatchcolors def _set_edgecolor(self, c): if c is None: - if (mpl.rcParams['patch.force_edgecolor'] - or self._edge_default - or cbook._str_equal(self._original_facecolor, 'none')): + if ( + mpl.rcParams["patch.force_edgecolor"] + or self._edge_default + or cbook._str_equal(self._original_facecolor, "none") + ): c = self._get_default_edgecolor() else: - c = 'none' - if cbook._str_lower_equal(c, 'face'): - self._edgecolors = 'face' + c = "none" + if cbook._str_lower_equal(c, "face"): + self._edgecolors = "face" self.stale = True return self._edgecolors = mcolors.to_rgba_array(c, self._alpha) @@ -911,9 +1008,9 @@ def set_edgecolor(self, c): self._set_edgecolor(c) def _set_hatchcolor(self, c): - c = mpl._val_or_rc(c, 'hatch.color') - if cbook._str_equal(c, 'edge'): - self._hatchcolors = 'edge' + c = mpl._val_or_rc(c, "hatch.color") + if cbook._str_equal(c, "edge"): + self._hatchcolors = "edge" else: self._hatchcolors = mcolors.to_rgba_array(c, self._alpha) self.stale = True @@ -976,18 +1073,21 @@ def _set_mappable_flags(self): self._edge_is_mapped = False self._face_is_mapped = False if self._A is not None: - if not cbook._str_equal(self._original_facecolor, 'none'): + if not cbook._str_equal(self._original_facecolor, "none"): self._face_is_mapped = True - if cbook._str_equal(self._original_edgecolor, 'face'): + if cbook._str_equal(self._original_edgecolor, "face"): self._edge_is_mapped = True else: if self._original_edgecolor is None: self._edge_is_mapped = True mapped = self._face_is_mapped or self._edge_is_mapped - changed = (edge0 is None or face0 is None - or self._edge_is_mapped != edge0 - or self._face_is_mapped != face0) + changed = ( + edge0 is None + or face0 is None + or self._edge_is_mapped != edge0 + or self._face_is_mapped != face0 + ) return mapped or changed def update_scalarmappable(self): @@ -1003,17 +1103,18 @@ def update_scalarmappable(self): if self._A is not None: # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array) if self._A.ndim > 1 and not isinstance(self, _MeshData): - raise ValueError('Collections can only map rank 1 arrays') + raise ValueError("Collections can only map rank 1 arrays") if np.iterable(self._alpha): if self._alpha.size != self._A.size: raise ValueError( - f'Data array shape, {self._A.shape} ' - 'is incompatible with alpha array shape, ' - f'{self._alpha.shape}. ' - 'This can occur with the deprecated ' + f"Data array shape, {self._A.shape} " + "is incompatible with alpha array shape, " + f"{self._alpha.shape}. " + "This can occur with the deprecated " 'behavior of the "flat" shading option, ' - 'in which a row and/or column of the data ' - 'array is dropped.') + "in which a row and/or column of the data " + "array is dropped." + ) # pcolormesh, scatter, maybe others flatten their _A self._alpha = self._alpha.reshape(self._A.shape) self._mapped_colors = self.to_rgba(self._A, self._alpha) @@ -1062,6 +1163,7 @@ class _CollectionWithSizes(Collection): """ Base class for collections that have an array of sizes. """ + _factor = 1.0 def get_sizes(self): @@ -1132,8 +1234,9 @@ def __init__(self, paths, sizes=None, **kwargs): def get_paths(self): return self._paths - def legend_elements(self, prop="colors", num="auto", - fmt=None, func=lambda x: x, **kwargs): + def legend_elements( + self, prop="colors", num="auto", fmt=None, func=lambda x: x, **kwargs + ): """ Create legend handles and labels for a PathCollection. @@ -1206,9 +1309,11 @@ def legend_elements(self, prop="colors", num="auto", if prop == "colors": if not hasarray: - warnings.warn("Collection without array used. Make sure to " - "specify the values to be colormapped via the " - "`c` argument.") + warnings.warn( + "Collection without array used. Make sure to " + "specify the values to be colormapped via the " + "`c` argument." + ) return handles, labels u = np.unique(self.get_array()) size = kwargs.pop("size", mpl.rcParams["lines.markersize"]) @@ -1216,8 +1321,10 @@ def legend_elements(self, prop="colors", num="auto", u = np.unique(self.get_sizes()) color = kwargs.pop("color", "k") else: - raise ValueError("Valid values for `prop` are 'colors' or " - f"'sizes'. You supplied '{prop}' instead.") + raise ValueError( + "Valid values for `prop` are 'colors' or " + f"'sizes'. You supplied '{prop}' instead." + ) fu = func(u) fmt.axis.set_view_interval(fu.min(), fu.max()) @@ -1240,20 +1347,22 @@ def legend_elements(self, prop="colors", num="auto", loc = mpl.ticker.FixedLocator(num) else: num = int(num) - loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1, - steps=[1, 2, 2.5, 3, 5, 6, 8, 10]) + loc = mpl.ticker.MaxNLocator( + nbins=num, min_n_ticks=num - 1, steps=[1, 2, 2.5, 3, 5, 6, 8, 10] + ) label_values = loc.tick_values(func(arr).min(), func(arr).max()) - cond = ((label_values >= func(arr).min()) & - (label_values <= func(arr).max())) + cond = (label_values >= func(arr).min()) & (label_values <= func(arr).max()) label_values = label_values[cond] yarr = np.linspace(arr.min(), arr.max(), 256) xarr = func(yarr) ix = np.argsort(xarr) values = np.interp(label_values, xarr[ix], yarr[ix]) - kw = {"markeredgewidth": self.get_linewidths()[0], - "alpha": self.get_alpha(), - **kwargs} + kw = { + "markeredgewidth": self.get_linewidths()[0], + "alpha": self.get_alpha(), + **kwargs, + } for val, lab in zip(values, label_values): if prop == "colors": @@ -1262,8 +1371,9 @@ def legend_elements(self, prop="colors", num="auto", size = np.sqrt(val) if np.isclose(size, 0.0): continue - h = mlines.Line2D([0], [0], ls="", color=color, ms=size, - marker=self.get_paths()[0], **kw) + h = mlines.Line2D( + [0], [0], ls="", color=color, ms=size, marker=self.get_paths()[0], **kw + ) handles.append(h) if hasattr(fmt, "set_locs"): fmt.set_locs(label_values) @@ -1328,17 +1438,29 @@ def set_verts(self, verts, closed=True): verts_pad = np.concatenate((verts, verts[:, :1]), axis=1) # It's faster to create the codes and internal flags once in a # template path and reuse them. - template_path = mpath.Path(verts_pad[0], closed=True) + if closed and verts.shape[1] >= 128: + template_path = mpath.Path(verts_pad[0], closed=False) + else: + template_path = mpath.Path(verts_pad[0], closed=True) codes = template_path.codes _make_path = mpath.Path._fast_from_codes_and_verts - self._paths = [_make_path(xy, codes, internals_from=template_path) - for xy in verts_pad] + self._paths = [ + _make_path(xy, codes, internals_from=template_path) for xy in verts_pad + ] return self._paths = [] for xy in verts: if len(xy): - self._paths.append(mpath.Path._create_closed(xy)) + if closed and len(xy) >= 128: + if isinstance(xy, np.ma.MaskedArray): + xy = xy.filled(np.nan) + xy = np.asanyarray(xy) + if len(xy) and (xy[0] != xy[-1]).any(): + xy = np.concatenate([xy, xy[:1]]) + self._paths.append(mpath.Path(xy, closed=False)) + else: + self._paths.append(mpath.Path._create_closed(xy)) else: self._paths.append(mpath.Path(xy)) @@ -1347,10 +1469,13 @@ def set_verts(self, verts, closed=True): def set_verts_and_codes(self, verts, codes): """Initialize vertices with path codes.""" if len(verts) != len(codes): - raise ValueError("'codes' must be a 1D list or array " - "with the same length of 'verts'") - self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) - for xy, cds in zip(verts, codes)] + raise ValueError( + "'codes' must be a 1D list or array " "with the same length of 'verts'" + ) + self._paths = [ + mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) + for xy, cds in zip(verts, codes) + ] self.stale = True @@ -1358,9 +1483,19 @@ class FillBetweenPolyCollection(PolyCollection): """ `.PolyCollection` that fills the area between two x- or y-curves. """ + def __init__( - self, t_direction, t, f1, f2, *, - where=None, interpolate=False, step=None, **kwargs): + self, + t_direction, + t, + f1, + f2, + *, + where=None, + interpolate=False, + step=None, + **kwargs, + ): """ Parameters ---------- @@ -1473,7 +1608,8 @@ def set_data(self, t, f1, f2, *, where=None): .PolyCollection.set_verts, .Line2D.set_data """ t, f1, f2 = self.axes._fill_between_process_units( - self.t_direction, self._f_direction, t, f1, f2) + self.t_direction, self._f_direction, t, f1, f2 + ) verts = self._make_verts(t, f1, f2, where) self.set_verts(verts) @@ -1481,8 +1617,11 @@ def set_data(self, t, f1, f2, *, where=None): def get_datalim(self, transData): """Calculate the data limits and return them as a `.Bbox`.""" datalim = transforms.Bbox.null() - datalim.update_from_data_xy((self.get_transform() - transData).transform( - np.concatenate([self._bbox, [self._bbox.minpos]]))) + datalim.update_from_data_xy( + (self.get_transform() - transData).transform( + np.concatenate([self._bbox, [self._bbox.minpos]]) + ) + ) return datalim def _make_verts(self, t, f1, f2, where): @@ -1495,8 +1634,13 @@ def _make_verts(self, t, f1, f2, where): t, f1, f2 = np.broadcast_arrays(np.atleast_1d(t), f1, f2, subok=True) self._bbox = transforms.Bbox.null() - self._bbox.update_from_data_xy(self._fix_pts_xy_order(np.concatenate([ - np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)]))) + self._bbox.update_from_data_xy( + self._fix_pts_xy_order( + np.concatenate( + [np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)] + ) + ) + ) return [ self._make_verts_for_region(t, f1, f2, idx0, idx1) @@ -1516,10 +1660,12 @@ def _get_data_mask(self, t, f1, f2, where): where = np.asarray(where, dtype=bool) if where.size != t.size: msg = "where size ({}) does not match {!r} size ({})".format( - where.size, self.t_direction, t.size) + where.size, self.t_direction, t.size + ) raise ValueError(msg) return where & ~functools.reduce( - np.logical_or, map(np.ma.getmaskarray, [t, f1, f2])) + np.logical_or, map(np.ma.getmaskarray, [t, f1, f2]) + ) @staticmethod def _validate_shapes(t_dir, f_dir, t, f1, f2): @@ -1530,7 +1676,8 @@ def _validate_shapes(t_dir, f_dir, t, f1, f2): raise ValueError(f"{name!r} is not 1-dimensional") if t.size > 1 and array.size > 1 and t.size != array.size: msg = "{!r} has size {}, but {!r} has an unequal size of {}".format( - t_dir, t.size, name, array.size) + t_dir, t.size, name, array.size + ) raise ValueError(msg) def _make_verts_for_region(self, t, f1, f2, idx0, idx1): @@ -1554,11 +1701,14 @@ def _make_verts_for_region(self, t, f1, f2, idx0, idx1): start = t_slice[0], f2_slice[0] end = t_slice[-1], f2_slice[-1] - pts = np.concatenate(( - np.asarray([start]), - np.stack((t_slice, f1_slice), axis=-1), - np.asarray([end]), - np.stack((t_slice, f2_slice), axis=-1)[::-1])) + pts = np.concatenate( + ( + np.asarray([start]), + np.stack((t_slice, f1_slice), axis=-1), + np.asarray([end]), + np.stack((t_slice, f2_slice), axis=-1)[::-1], + ) + ) return self._fix_pts_xy_order(pts) @@ -1566,9 +1716,9 @@ def _make_verts_for_region(self, t, f1, f2, idx0, idx1): def _get_interpolating_points(cls, t, f1, f2, idx): """Calculate interpolating points.""" im1 = max(idx - 1, 0) - t_values = t[im1:idx+1] - diff_values = f1[im1:idx+1] - f2[im1:idx+1] - f1_values = f1[im1:idx+1] + t_values = t[im1 : idx + 1] + diff_values = f1[im1 : idx + 1] - f2[im1 : idx + 1] + f1_values = f1[im1 : idx + 1] if len(diff_values) == 2: if np.ma.is_masked(diff_values[1]): @@ -1600,14 +1750,9 @@ class RegularPolyCollection(_CollectionWithSizes): """A collection of n-sided regular polygons.""" _path_generator = mpath.Path.unit_regular_polygon - _factor = np.pi ** (-1/2) + _factor = np.pi ** (-1 / 2) - def __init__(self, - numsides, - *, - rotation=0, - sizes=(1,), - **kwargs): + def __init__(self, numsides, *, rotation=0, sizes=(1,), **kwargs): """ Parameters ---------- @@ -1664,11 +1809,13 @@ def draw(self, renderer): class StarPolygonCollection(RegularPolyCollection): """Draw a collection of regular stars with *numsides* points.""" + _path_generator = mpath.Path.unit_regular_star class AsteriskPolygonCollection(RegularPolyCollection): """Draw a collection of regular asterisks with *numsides* points.""" + _path_generator = mpath.Path.unit_regular_asterisk @@ -1692,11 +1839,9 @@ class LineCollection(Collection): _edge_default = True - def __init__(self, segments, # Can be None. - *, - zorder=2, # Collection.zorder is 1 - **kwargs - ): + def __init__( + self, segments, *, zorder=2, **kwargs # Can be None. # Collection.zorder is 1 + ): """ Parameters ---------- @@ -1730,19 +1875,22 @@ def __init__(self, segments, # Can be None. Forwarded to `.Collection`. """ # Unfortunately, mplot3d needs this explicit setting of 'facecolors'. - kwargs.setdefault('facecolors', 'none') - super().__init__( - zorder=zorder, - **kwargs) + kwargs.setdefault("facecolors", "none") + super().__init__(zorder=zorder, **kwargs) self.set_segments(segments) def set_segments(self, segments): if segments is None: return - self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray) - else mpath.Path(np.asarray(seg, float)) - for seg in segments] + self._paths = [ + ( + mpath.Path(seg) + if isinstance(seg, np.ma.MaskedArray) + else mpath.Path(np.asarray(seg, float)) + ) + for seg in segments + ] self.stale = True set_verts = set_segments # for compatibility with PolyCollection @@ -1773,16 +1921,16 @@ def get_segments(self): return segments def _get_default_linewidth(self): - return mpl.rcParams['lines.linewidth'] + return mpl.rcParams["lines.linewidth"] def _get_default_antialiased(self): - return mpl.rcParams['lines.antialiased'] + return mpl.rcParams["lines.antialiased"] def _get_default_edgecolor(self): - return mpl.rcParams['lines.color'] + return mpl.rcParams["lines.color"] def _get_default_facecolor(self): - return 'none' + return "none" def set_alpha(self, alpha): # docstring inherited @@ -1849,11 +1997,13 @@ def _get_inverse_paths_linestyles(self): to nans to prevent drawing an inverse line. """ path_patterns = [ - (mpath.Path(np.full((1, 2), np.nan)), ls) - if ls == (0, None) else - (path, mlines._get_inverse_dash_pattern(*ls)) - for (path, ls) in - zip(self._paths, itertools.cycle(self._linestyles))] + ( + (mpath.Path(np.full((1, 2), np.nan)), ls) + if ls == (0, None) + else (path, mlines._get_inverse_dash_pattern(*ls)) + ) + for (path, ls) in zip(self._paths, itertools.cycle(self._linestyles)) + ] return zip(*path_patterns) @@ -1868,18 +2018,19 @@ class EventCollection(LineCollection): _edge_default = True - def __init__(self, - positions, # Cannot be None. - orientation='horizontal', - *, - lineoffset=0, - linelength=1, - linewidth=None, - color=None, - linestyle='solid', - antialiased=None, - **kwargs - ): + def __init__( + self, + positions, # Cannot be None. + orientation="horizontal", + *, + lineoffset=0, + linelength=1, + linewidth=None, + color=None, + linestyle="solid", + antialiased=None, + **kwargs, + ): """ Parameters ---------- @@ -1916,10 +2067,14 @@ def __init__(self, -------- .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py """ - super().__init__([], - linewidths=linewidth, linestyles=linestyle, - colors=color, antialiaseds=antialiased, - **kwargs) + super().__init__( + [], + linewidths=linewidth, + linestyles=linestyle, + colors=color, + antialiaseds=antialiased, + **kwargs, + ) self._is_horizontal = True # Initial value, may be switched below. self._linelength = linelength self._lineoffset = lineoffset @@ -1938,7 +2093,7 @@ def set_positions(self, positions): if positions is None: positions = [] if np.ndim(positions) != 1: - raise ValueError('positions must be one-dimensional') + raise ValueError("positions must be one-dimensional") lineoffset = self.get_lineoffset() linelength = self.get_linelength() pos_idx = 0 if self.is_horizontal() else 1 @@ -1950,12 +2105,12 @@ def set_positions(self, positions): def add_positions(self, position): """Add one or more events at the specified positions.""" - if position is None or (hasattr(position, 'len') and - len(position) == 0): + if position is None or (hasattr(position, "len") and len(position) == 0): return positions = self.get_positions() positions = np.hstack([positions, np.asanyarray(position)]) self.set_positions(positions) + extend_positions = append_positions = add_positions def is_horizontal(self): @@ -1966,7 +2121,7 @@ def get_orientation(self): """ Return the orientation of the event line ('horizontal' or 'vertical'). """ - return 'horizontal' if self.is_horizontal() else 'vertical' + return "horizontal" if self.is_horizontal() else "vertical" def switch_orientation(self): """ @@ -1989,8 +2144,8 @@ def set_orientation(self, orientation): orientation : {'horizontal', 'vertical'} """ is_horizontal = _api.check_getitem( - {"horizontal": True, "vertical": False}, - orientation=orientation) + {"horizontal": True, "vertical": False}, orientation=orientation + ) if is_horizontal == self.is_horizontal(): return self.switch_orientation() @@ -2007,8 +2162,8 @@ def set_linelength(self, linelength): segments = self.get_segments() pos = 1 if self.is_horizontal() else 0 for segment in segments: - segment[0, pos] = lineoffset + linelength / 2. - segment[1, pos] = lineoffset - linelength / 2. + segment[0, pos] = lineoffset + linelength / 2.0 + segment[1, pos] = lineoffset - linelength / 2.0 self.set_segments(segments) self._linelength = linelength @@ -2024,8 +2179,8 @@ def set_lineoffset(self, lineoffset): segments = self.get_segments() pos = 1 if self.is_horizontal() else 0 for segment in segments: - segment[0, pos] = lineoffset + linelength / 2. - segment[1, pos] = lineoffset - linelength / 2. + segment[0, pos] = lineoffset + linelength / 2.0 + segment[1, pos] = lineoffset - linelength / 2.0 self.set_segments(segments) self._lineoffset = lineoffset @@ -2044,7 +2199,7 @@ def get_color(self): class CircleCollection(_CollectionWithSizes): """A collection of circles, drawn using splines.""" - _factor = np.pi ** (-1/2) + _factor = np.pi ** (-1 / 2) def __init__(self, sizes, **kwargs): """ @@ -2064,7 +2219,7 @@ def __init__(self, sizes, **kwargs): class EllipseCollection(Collection): """A collection of ellipses, drawn using splines.""" - def __init__(self, widths, heights, angles, *, units='points', **kwargs): + def __init__(self, widths, heights, angles, *, units="points", **kwargs): """ Parameters ---------- @@ -2100,24 +2255,24 @@ def _set_transforms(self): ax = self.axes fig = self.get_figure(root=False) - if self._units == 'xy': + if self._units == "xy": sc = 1 - elif self._units == 'x': + elif self._units == "x": sc = ax.bbox.width / ax.viewLim.width - elif self._units == 'y': + elif self._units == "y": sc = ax.bbox.height / ax.viewLim.height - elif self._units == 'inches': + elif self._units == "inches": sc = fig.dpi - elif self._units == 'points': + elif self._units == "points": sc = fig.dpi / 72.0 - elif self._units == 'width': + elif self._units == "width": sc = ax.bbox.width - elif self._units == 'height': + elif self._units == "height": sc = ax.bbox.height - elif self._units == 'dots': + elif self._units == "dots": sc = 1.0 else: - raise ValueError(f'Unrecognized units: {self._units!r}') + raise ValueError(f"Unrecognized units: {self._units!r}") self._transforms = np.zeros((len(self._widths), 3, 3)) widths = self._widths * sc @@ -2131,7 +2286,7 @@ def _set_transforms(self): self._transforms[:, 2, 2] = 1.0 _affine = transforms.Affine2D - if self._units == 'xy': + if self._units == "xy": m = ax.transData.get_affine().get_matrix().copy() m[:2, 2:] = 0 self.set_transform(_affine(m)) @@ -2208,24 +2363,24 @@ def __init__(self, patches, *, match_original=False, **kwargs): """ if match_original: + def determine_facecolor(patch): if patch.get_fill(): return patch.get_facecolor() return [0, 0, 0, 0] - kwargs['facecolors'] = [determine_facecolor(p) for p in patches] - kwargs['edgecolors'] = [p.get_edgecolor() for p in patches] - kwargs['linewidths'] = [p.get_linewidth() for p in patches] - kwargs['linestyles'] = [p.get_linestyle() for p in patches] - kwargs['antialiaseds'] = [p.get_antialiased() for p in patches] + kwargs["facecolors"] = [determine_facecolor(p) for p in patches] + kwargs["edgecolors"] = [p.get_edgecolor() for p in patches] + kwargs["linewidths"] = [p.get_linewidth() for p in patches] + kwargs["linestyles"] = [p.get_linestyle() for p in patches] + kwargs["antialiaseds"] = [p.get_antialiased() for p in patches] super().__init__(**kwargs) self.set_paths(patches) def set_paths(self, patches): - paths = [p.get_transform().transform_path(p.get_path()) - for p in patches] + paths = [p.get_transform().transform_path(p.get_path()) for p in patches] self._paths = paths @@ -2235,17 +2390,17 @@ class TriMesh(Collection): A triangular mesh is a `~matplotlib.tri.Triangulation` object. """ + def __init__(self, triangulation, **kwargs): super().__init__(**kwargs) self._triangulation = triangulation - self._shading = 'gouraud' + self._shading = "gouraud" self._bbox = transforms.Bbox.unit() # Unfortunately this requires a copy, unless Triangulation # was rewritten. - xy = np.hstack((triangulation.x.reshape(-1, 1), - triangulation.y.reshape(-1, 1))) + xy = np.hstack((triangulation.x.reshape(-1, 1), triangulation.y.reshape(-1, 1))) self._bbox.update_from_data_xy(xy) def get_paths(self): @@ -2318,7 +2473,8 @@ class _MeshData: shading : {'flat', 'gouraud'}, default: 'flat' """ - def __init__(self, coordinates, *, shading='flat'): + + def __init__(self, coordinates, *, shading="flat"): _api.check_shape((None, None, 2), coordinates=coordinates) self._coordinates = coordinates self._shading = shading @@ -2346,7 +2502,7 @@ def set_array(self, A): shading. """ height, width = self._coordinates.shape[0:-1] - if self._shading == 'flat': + if self._shading == "flat": h, w = height - 1, width - 1 else: h, w = height, width @@ -2357,7 +2513,8 @@ def set_array(self, A): raise ValueError( f"For X ({width}) and Y ({height}) with {self._shading} " f"shading, A should have shape " - f"{' or '.join(map(str, ok_shapes))}, not {A.shape}") + f"{' or '.join(map(str, ok_shapes))}, not {A.shape}" + ) return super().set_array(A) def get_coordinates(self): @@ -2394,13 +2551,9 @@ def _convert_mesh_to_paths(coordinates): c = coordinates.data else: c = coordinates - points = np.concatenate([ - c[:-1, :-1], - c[:-1, 1:], - c[1:, 1:], - c[1:, :-1], - c[:-1, :-1] - ], axis=2).reshape((-1, 5, 2)) + points = np.concatenate( + [c[:-1, :-1], c[:-1, 1:], c[1:, 1:], c[1:, :-1], c[:-1, :-1]], axis=2 + ).reshape((-1, 5, 2)) return [mpath.Path(x) for x in points] def _convert_mesh_to_triangles(self, coordinates): @@ -2419,12 +2572,23 @@ def _convert_mesh_to_triangles(self, coordinates): p_c = p[1:, 1:] p_d = p[1:, :-1] p_center = (p_a + p_b + p_c + p_d) / 4.0 - triangles = np.concatenate([ - p_a, p_b, p_center, - p_b, p_c, p_center, - p_c, p_d, p_center, - p_d, p_a, p_center, - ], axis=2).reshape((-1, 3, 2)) + triangles = np.concatenate( + [ + p_a, + p_b, + p_center, + p_b, + p_c, + p_center, + p_c, + p_d, + p_center, + p_d, + p_a, + p_center, + ], + axis=2, + ).reshape((-1, 3, 2)) c = self.get_facecolor().reshape((*coordinates.shape[:2], 4)) z = self.get_array() @@ -2436,12 +2600,23 @@ def _convert_mesh_to_triangles(self, coordinates): c_c = c[1:, 1:] c_d = c[1:, :-1] c_center = (c_a + c_b + c_c + c_d) / 4.0 - colors = np.concatenate([ - c_a, c_b, c_center, - c_b, c_c, c_center, - c_c, c_d, c_center, - c_d, c_a, c_center, - ], axis=2).reshape((-1, 3, 4)) + colors = np.concatenate( + [ + c_a, + c_b, + c_center, + c_b, + c_c, + c_center, + c_c, + c_d, + c_center, + c_d, + c_a, + c_center, + ], + axis=2, + ).reshape((-1, 3, 4)) tmask = np.isnan(colors[..., 2, 3]) return triangles[~tmask], colors[~tmask] @@ -2480,8 +2655,7 @@ class QuadMesh(_MeshData, Collection): """ - def __init__(self, coordinates, *, antialiased=True, shading='flat', - **kwargs): + def __init__(self, coordinates, *, antialiased=True, shading="flat", **kwargs): kwargs.setdefault("pickradius", 0) super().__init__(coordinates=coordinates, shading=shading) Collection.__init__(self, **kwargs) @@ -2536,18 +2710,23 @@ def draw(self, renderer): self._set_gc_clip(gc) gc.set_linewidth(self.get_linewidth()[0]) - if self._shading == 'gouraud': + if self._shading == "gouraud": triangles, colors = self._convert_mesh_to_triangles(coordinates) - renderer.draw_gouraud_triangles( - gc, triangles, colors, transform.frozen()) + renderer.draw_gouraud_triangles(gc, triangles, colors, transform.frozen()) else: renderer.draw_quad_mesh( - gc, transform.frozen(), - coordinates.shape[1] - 1, coordinates.shape[0] - 1, - coordinates, offsets, offset_trf, + gc, + transform.frozen(), + coordinates.shape[1] - 1, + coordinates.shape[0] - 1, + coordinates, + offsets, + offset_trf, # Backends expect flattened rgba arrays (n*m, 4) for fc and ec self.get_facecolor().reshape((-1, 4)), - self._antialiased, self.get_edgecolors().reshape((-1, 4))) + self._antialiased, + self.get_edgecolors().reshape((-1, 4)), + ) gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False @@ -2606,7 +2785,7 @@ def _get_unmasked_polys(self): mask = np.any(np.ma.getmaskarray(self._coordinates), axis=-1) # We want the shape of the polygon, which is the corner of each X/Y array - mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) + mask = mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1] arr = self.get_array() if arr is not None: arr = np.ma.getmaskarray(arr) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 4ec78758e598..9216f11b0675 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -212,12 +212,7 @@ def _update_values(self): self._simplify_threshold > 0 and mpl.rcParams["path.simplify"] and len(self._vertices) >= 128 - and ( - self._codes is None - or np.all( - (self._codes <= Path.LINETO) | (self._codes == Path.CLOSEPOLY) - ) - ) + and (self._codes is None or np.all(self._codes <= Path.LINETO)) ) @property From 79a2a99ae9a8fae9905c6bb7085cbc193d41fe3b Mon Sep 17 00:00:00 2001 From: DavidAG Date: Wed, 17 Dec 2025 19:22:02 +0100 Subject: [PATCH 3/3] Revert all changes except test_path.py deprecation fix --- lib/matplotlib/backends/backend_pdf.py | 1574 ++++++++++-------------- lib/matplotlib/backends/backend_svg.py | 992 +++++++-------- lib/matplotlib/collections.py | 727 +++++------ lib/matplotlib/path.py | 321 +++-- 4 files changed, 1495 insertions(+), 2119 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index f5c2b95bcbb9..d63808eb3925 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -29,12 +29,8 @@ from matplotlib import _api, _text_helpers, _type1font, cbook, dviread from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( - _Backend, - FigureCanvasBase, - FigureManagerBase, - GraphicsContextBase, - RendererBase, -) + _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, + RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager @@ -111,11 +107,11 @@ def _fill(strings, linelen=75): if currpos + length < linelen: currpos += length + 1 else: - result.append(b" ".join(strings[lasti:i])) + result.append(b' '.join(strings[lasti:i])) lasti = i currpos = length - result.append(b" ".join(strings[lasti:])) - return b"\n".join(result) + result.append(b' '.join(strings[lasti:])) + return b'\n'.join(result) def _create_pdf_info_dict(backend, metadata): @@ -158,56 +154,49 @@ def _create_pdf_info_dict(backend, metadata): source_date = datetime.today() info = { - "Creator": f"Matplotlib v{mpl.__version__}, https://matplotlib.org", - "Producer": f"Matplotlib {backend} backend v{mpl.__version__}", - "CreationDate": source_date, - **metadata, + 'Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org', + 'Producer': f'Matplotlib {backend} backend v{mpl.__version__}', + 'CreationDate': source_date, + **metadata } info = {k: v for (k, v) in info.items() if v is not None} def is_string_like(x): return isinstance(x, str) - is_string_like.text_for_warning = "an instance of str" def is_date(x): return isinstance(x, datetime) - is_date.text_for_warning = "an instance of datetime.datetime" def check_trapped(x): if isinstance(x, Name): - return x.name in (b"True", b"False", b"Unknown") + return x.name in (b'True', b'False', b'Unknown') else: - return x in ("True", "False", "Unknown") - + return x in ('True', 'False', 'Unknown') check_trapped.text_for_warning = 'one of {"True", "False", "Unknown"}' keywords = { - "Title": is_string_like, - "Author": is_string_like, - "Subject": is_string_like, - "Keywords": is_string_like, - "Creator": is_string_like, - "Producer": is_string_like, - "CreationDate": is_date, - "ModDate": is_date, - "Trapped": check_trapped, + 'Title': is_string_like, + 'Author': is_string_like, + 'Subject': is_string_like, + 'Keywords': is_string_like, + 'Creator': is_string_like, + 'Producer': is_string_like, + 'CreationDate': is_date, + 'ModDate': is_date, + 'Trapped': check_trapped, } for k in info: if k not in keywords: - _api.warn_external( - f"Unknown infodict keyword: {k!r}. " - f"Must be one of {set(keywords)!r}." - ) + _api.warn_external(f'Unknown infodict keyword: {k!r}. ' + f'Must be one of {set(keywords)!r}.') elif not keywords[k](info[k]): - _api.warn_external( - f"Bad value for infodict keyword {k}. " - f"Got {info[k]!r} which is not " - f"{keywords[k].text_for_warning}." - ) - if "Trapped" in info: - info["Trapped"] = Name(info["Trapped"]) + _api.warn_external(f'Bad value for infodict keyword {k}. ' + f'Got {info[k]!r} which is not ' + f'{keywords[k].text_for_warning}.') + if 'Trapped' in info: + info['Trapped'] = Name(info['Trapped']) return info @@ -218,7 +207,7 @@ def _datetime_to_pdf(d): Used for PDF and PGF. """ - r = d.strftime("D:%Y%m%d%H%M%S") + r = d.strftime('D:%Y%m%d%H%M%S') z = d.utcoffset() if z is not None: z = z.seconds @@ -228,7 +217,7 @@ def _datetime_to_pdf(d): else: z = time.timezone if z == 0: - r += "Z" + r += 'Z' elif z < 0: r += "+%02d'%02d'" % ((-z) // 3600, (-z) % 3600) else: @@ -259,7 +248,8 @@ def _get_coordinates_of_block(x, y, width, height, angle=0): rotated rectangle. """ - vertices = _calculate_quad_point_coordinates(x, y, width, height, angle) + vertices = _calculate_quad_point_coordinates(x, y, width, + height, angle) # Find min and max values for rectangle # adjust so that QuadPoints is inside Rect @@ -272,10 +262,8 @@ def _get_coordinates_of_block(x, y, width, height, angle=0): min_y = min(v[1] for v in vertices) - pad max_x = max(v[0] for v in vertices) + pad max_y = max(v[1] for v in vertices) + pad - return ( - tuple(itertools.chain.from_iterable(vertices)), - (min_x, min_y, max_x, max_y), - ) + return (tuple(itertools.chain.from_iterable(vertices)), + (min_x, min_y, max_x, max_y)) def _get_link_annotation(gc, x, y, width, height, angle=0): @@ -284,18 +272,18 @@ def _get_link_annotation(gc, x, y, width, height, angle=0): """ quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle) link_annotation = { - "Type": Name("Annot"), - "Subtype": Name("Link"), - "Rect": rect, - "Border": [0, 0, 0], - "A": { - "S": Name("URI"), - "URI": gc.get_url(), + 'Type': Name('Annot'), + 'Subtype': Name('Link'), + 'Rect': rect, + 'Border': [0, 0, 0], + 'A': { + 'S': Name('URI'), + 'URI': gc.get_url(), }, } if angle % 90: # Add QuadPoints - link_annotation["QuadPoints"] = quadpoints + link_annotation['QuadPoints'] = quadpoints return link_annotation @@ -304,16 +292,15 @@ def _get_link_annotation(gc, x, y, width, height, angle=0): # However, sf bug #2708559 shows that the carriage return character may get # read as a newline; these characters correspond to \gamma and \Omega in TeX's # math font encoding. Escaping them fixes the bug. -_str_escapes = str.maketrans( - {"\\": "\\\\", "(": "\\(", ")": "\\)", "\n": "\\n", "\r": "\\r"} -) +_str_escapes = str.maketrans({ + '\\': '\\\\', '(': '\\(', ')': '\\)', '\n': '\\n', '\r': '\\r'}) def pdfRepr(obj): """Map Python objects to PDF syntax.""" # Some objects defined later have their own pdfRepr method. - if hasattr(obj, "pdfRepr"): + if hasattr(obj, 'pdfRepr'): return obj.pdfRepr() # Floats. PDF does not have exponential notation (1.0e-10) so we @@ -323,12 +310,12 @@ def pdfRepr(obj): if not np.isfinite(obj): raise ValueError("Can only output finite numbers in PDF") r = b"%.10f" % obj - return r.rstrip(b"0").rstrip(b".") + return r.rstrip(b'0').rstrip(b'.') # Booleans. Needs to be tested before integers since # isinstance(True, int) is true. elif isinstance(obj, bool): - return [b"false", b"true"][obj] + return [b'false', b'true'][obj] # Integers are written as such. elif isinstance(obj, (int, np.integer)): @@ -336,11 +323,8 @@ def pdfRepr(obj): # Non-ASCII Unicode strings are encoded in UTF-16BE with byte-order mark. elif isinstance(obj, str): - return pdfRepr( - obj.encode("ascii") - if obj.isascii() - else codecs.BOM_UTF16_BE + obj.encode("UTF-16BE") - ) + return pdfRepr(obj.encode('ascii') if obj.isascii() + else codecs.BOM_UTF16_BE + obj.encode('UTF-16BE')) # Strings are written in parentheses, with backslashes and parens # escaped. Actually balanced parens are allowed, but it is @@ -349,23 +333,20 @@ def pdfRepr(obj): # Despite the extra decode/encode, translate is faster than regex. elif isinstance(obj, bytes): return ( - b"(" - + obj.decode("latin-1").translate(_str_escapes).encode("latin-1") - + b")" - ) + b'(' + + obj.decode('latin-1').translate(_str_escapes).encode('latin-1') + + b')') # Dictionaries. The keys must be PDF names, so if we find strings # there, we make Name objects from them. The values may be # anything, so the caller must ensure that PDF names are # represented as Name objects. elif isinstance(obj, dict): - return _fill( - [ - b"<<", - *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], - b">>", - ] - ) + return _fill([ + b"<<", + *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], + b">>", + ]) # Lists. elif isinstance(obj, (list, tuple)): @@ -373,7 +354,7 @@ def pdfRepr(obj): # The null keyword. elif obj is None: - return b"null" + return b'null' # A date. elif isinstance(obj, datetime): @@ -384,7 +365,8 @@ def pdfRepr(obj): return _fill([pdfRepr(val) for val in obj.bounds]) else: - raise TypeError(f"Don't know a PDF representation for {type(obj)} " "objects") + raise TypeError(f"Don't know a PDF representation for {type(obj)} " + "objects") def _font_supports_glyph(fonttype, glyph): @@ -428,23 +410,23 @@ def write(self, contents, file): @total_ordering class Name: """PDF name object.""" - - __slots__ = ("name",) - _hexify = {c: "#%02x" % c for c in {*range(256)} - {*range(ord("!"), ord("~") + 1)}} + __slots__ = ('name',) + _hexify = {c: '#%02x' % c + for c in {*range(256)} - {*range(ord('!'), ord('~') + 1)}} def __init__(self, name): if isinstance(name, Name): self.name = name.name else: if isinstance(name, bytes): - name = name.decode("ascii") - self.name = name.translate(self._hexify).encode("ascii") + name = name.decode('ascii') + self.name = name.translate(self._hexify).encode('ascii') def __repr__(self): return "" % self.name def __str__(self): - return "/" + self.name.decode("ascii") + return '/' + self.name.decode('ascii') def __eq__(self, other): return isinstance(other, Name) and self.name == other.name @@ -456,12 +438,11 @@ def __hash__(self): return hash(self.name) def pdfRepr(self): - return b"/" + self.name + return b'/' + self.name class Verbatim: """Store verbatim PDF command content for later inclusion in the stream.""" - def __init__(self, x): self._x = x @@ -472,43 +453,43 @@ def pdfRepr(self): class Op(Enum): """PDF operators (not an exhaustive list).""" - close_fill_stroke = b"b" - fill_stroke = b"B" - fill = b"f" - closepath = b"h" - close_stroke = b"s" - stroke = b"S" - endpath = b"n" - begin_text = b"BT" - end_text = b"ET" - curveto = b"c" - rectangle = b"re" - lineto = b"l" - moveto = b"m" - concat_matrix = b"cm" - use_xobject = b"Do" - setgray_stroke = b"G" - setgray_nonstroke = b"g" - setrgb_stroke = b"RG" - setrgb_nonstroke = b"rg" - setcolorspace_stroke = b"CS" - setcolorspace_nonstroke = b"cs" - setcolor_stroke = b"SCN" - setcolor_nonstroke = b"scn" - setdash = b"d" - setlinejoin = b"j" - setlinecap = b"J" - setgstate = b"gs" - gsave = b"q" - grestore = b"Q" - textpos = b"Td" - selectfont = b"Tf" - textmatrix = b"Tm" - show = b"Tj" - showkern = b"TJ" - setlinewidth = b"w" - clip = b"W" - shading = b"sh" + close_fill_stroke = b'b' + fill_stroke = b'B' + fill = b'f' + closepath = b'h' + close_stroke = b's' + stroke = b'S' + endpath = b'n' + begin_text = b'BT' + end_text = b'ET' + curveto = b'c' + rectangle = b're' + lineto = b'l' + moveto = b'm' + concat_matrix = b'cm' + use_xobject = b'Do' + setgray_stroke = b'G' + setgray_nonstroke = b'g' + setrgb_stroke = b'RG' + setrgb_nonstroke = b'rg' + setcolorspace_stroke = b'CS' + setcolorspace_nonstroke = b'cs' + setcolor_stroke = b'SCN' + setcolor_nonstroke = b'scn' + setdash = b'd' + setlinejoin = b'j' + setlinecap = b'J' + setgstate = b'gs' + gsave = b'q' + grestore = b'Q' + textpos = b'Td' + selectfont = b'Tf' + textmatrix = b'Tm' + show = b'Tj' + showkern = b'TJ' + setlinewidth = b'w' + clip = b'W' + shading = b'sh' def pdfRepr(self): return self.value @@ -544,8 +525,7 @@ class Stream: This has no pdfRepr method. Instead, call begin(), then output the contents of the stream by calling write(), and finally call end(). """ - - __slots__ = ("id", "len", "pdfFile", "file", "compressobj", "extra", "pos") + __slots__ = ('id', 'len', 'pdfFile', 'file', 'compressobj', 'extra', 'pos') def __init__(self, id, len, file, extra=None, png=None): """ @@ -563,21 +543,23 @@ def __init__(self, id, len, file, extra=None, png=None): png : dict or None If the data is already png encoded, the decode parameters. """ - self.id = id # object id - self.len = len # id of length object + self.id = id # object id + self.len = len # id of length object self.pdfFile = file - self.file = file.fh # file to which the stream is written + self.file = file.fh # file to which the stream is written self.compressobj = None # compression object if extra is None: self.extra = dict() else: self.extra = extra.copy() if png is not None: - self.extra.update({"Filter": Name("FlateDecode"), "DecodeParms": png}) + self.extra.update({'Filter': Name('FlateDecode'), + 'DecodeParms': png}) self.pdfFile.recordXref(self.id) - if mpl.rcParams["pdf.compression"] and not png: - self.compressobj = zlib.compressobj(mpl.rcParams["pdf.compression"]) + if mpl.rcParams['pdf.compression'] and not png: + self.compressobj = zlib.compressobj( + mpl.rcParams['pdf.compression']) if self.len is None: self.file = BytesIO() else: @@ -588,9 +570,9 @@ def _writeHeader(self): write = self.file.write write(b"%d 0 obj\n" % self.id) dict = self.extra - dict["Length"] = self.len - if mpl.rcParams["pdf.compression"]: - dict["Filter"] = Name("FlateDecode") + dict['Length'] = self.len + if mpl.rcParams['pdf.compression']: + dict['Filter'] = Name('FlateDecode') write(pdfRepr(dict)) write(b"\nstream\n") @@ -638,7 +620,7 @@ def _get_pdf_charprocs(font_path, glyph_ids): # NOTE: We should be using round(), but instead use # "(x+.5).astype(int)" to keep backcompat with the old ttconv code # (this is different for negative x's). - d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + 0.5).astype(int) + d1 = (np.array([g.horiAdvance, 0, *g.bbox]) * conv + .5).astype(int) v, c = font.get_path() v = (v * 64).astype(int) # Back to TrueType's internal units (1/64's). # Backcompat with old ttconv code: control points between two quads are @@ -649,42 +631,29 @@ def _get_pdf_charprocs(font_path, glyph_ids): # re-interpolating them. Note that occasionally (e.g. with DejaVu Sans # glyph "0") a point detected as "implicit" is actually explicit, and # will thus be shifted by 1. - (quads,) = np.nonzero(c == 3) + quads, = np.nonzero(c == 3) quads_on = quads[1::2] quads_mid_on = np.array( - sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int - ) + sorted({*quads_on} & {*(quads - 1)} & {*(quads + 1)}), int) implicit = quads_mid_on[ - ( - v[quads_mid_on] # As above, use astype(int), not // division - == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int) - ).all(axis=1) - ] + (v[quads_mid_on] # As above, use astype(int), not // division + == ((v[quads_mid_on - 1] + v[quads_mid_on + 1]) / 2).astype(int)) + .all(axis=1)] if (font.postscript_name, glyph_id) in [ - ("DejaVuSerif-Italic", 77), # j - ("DejaVuSerif-Italic", 135), # \AA + ("DejaVuSerif-Italic", 77), # j + ("DejaVuSerif-Italic", 135), # \AA ]: v[:, 0] -= 1 # Hard-coded backcompat (FreeType shifts glyph by 1). - v = (v * conv + 0.5).astype(int) # As above re: truncation vs rounding. - v[implicit] = ( # Fix implicit points; again, truncate. - (v[implicit - 1] + v[implicit + 1]) / 2 - ).astype(int) + v = (v * conv + .5).astype(int) # As above re: truncation vs rounding. + v[implicit] = (( # Fix implicit points; again, truncate. + (v[implicit - 1] + v[implicit + 1]) / 2).astype(int)) procs[font.get_glyph_name(glyph_id)] = ( - " ".join(map(str, d1)).encode("ascii") - + b" d1\n" + " ".join(map(str, d1)).encode("ascii") + b" d1\n" + _path.convert_to_string( - Path(v, c), - None, - None, - False, - None, - -1, + Path(v, c), None, None, False, None, -1, # no code for quad Beziers triggers auto-conversion to cubics. - [b"m", b"l", b"", b"c", b"h"], - True, - ) - + b"f" - ) + [b"m", b"l", b"", b"c", b"h"], True) + + b"f") return procs @@ -711,7 +680,7 @@ def __init__(self, filename, metadata=None): super().__init__() self._object_seq = itertools.count(1) # consumed by reserveObject - self.xrefTable = [[0, 65535, "the zero object"]] + self.xrefTable = [[0, 65535, 'the zero object']] self.passed_in_file_object = False self.original_file_like = None self.tell_base = 0 @@ -728,43 +697,44 @@ def __init__(self, filename, metadata=None): self.fh = fh self.currentstream = None # stream object to write to, if any - fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha + fh.write(b"%PDF-1.4\n") # 1.4 is the first version to have alpha # Output some eight-bit chars as a comment so various utilities # recognize the file as binary by looking at the first few # lines (see note in section 3.4.1 of the PDF reference). fh.write(b"%\254\334 \253\272\n") - self.rootObject = self.reserveObject("root") - self.pagesObject = self.reserveObject("pages") + self.rootObject = self.reserveObject('root') + self.pagesObject = self.reserveObject('pages') self.pageList = [] - self.fontObject = self.reserveObject("fonts") - self._extGStateObject = self.reserveObject("extended graphics states") - self.hatchObject = self.reserveObject("tiling patterns") - self.gouraudObject = self.reserveObject("Gouraud triangles") - self.XObjectObject = self.reserveObject("external objects") - self.resourceObject = self.reserveObject("resources") - - root = {"Type": Name("Catalog"), "Pages": self.pagesObject} + self.fontObject = self.reserveObject('fonts') + self._extGStateObject = self.reserveObject('extended graphics states') + self.hatchObject = self.reserveObject('tiling patterns') + self.gouraudObject = self.reserveObject('Gouraud triangles') + self.XObjectObject = self.reserveObject('external objects') + self.resourceObject = self.reserveObject('resources') + + root = {'Type': Name('Catalog'), + 'Pages': self.pagesObject} self.writeObject(self.rootObject, root) - self.infoDict = _create_pdf_info_dict("pdf", metadata or {}) + self.infoDict = _create_pdf_info_dict('pdf', metadata or {}) - self._internal_font_seq = (Name(f"F{i}") for i in itertools.count(1)) - self._fontNames = {} # maps filenames to internal font names - self._dviFontInfo = {} # maps pdf names to dvifonts + self._internal_font_seq = (Name(f'F{i}') for i in itertools.count(1)) + self._fontNames = {} # maps filenames to internal font names + self._dviFontInfo = {} # maps pdf names to dvifonts self._character_tracker = _backend_pdf_ps.CharacterTracker() - self.alphaStates = {} # maps alpha values to graphics state objects - self._alpha_state_seq = (Name(f"A{i}") for i in itertools.count(1)) + self.alphaStates = {} # maps alpha values to graphics state objects + self._alpha_state_seq = (Name(f'A{i}') for i in itertools.count(1)) self._soft_mask_states = {} - self._soft_mask_seq = (Name(f"SM{i}") for i in itertools.count(1)) + self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) self._soft_mask_groups = [] self._hatch_patterns = {} - self._hatch_pattern_seq = (Name(f"H{i}") for i in itertools.count(1)) + self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) self.gouraudTriangles = [] self._images = {} - self._image_seq = (Name(f"I{i}") for i in itertools.count(1)) + self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) self.markers = {} self.multi_byte_charprocs = {} @@ -785,14 +755,12 @@ def __init__(self, filename, metadata=None): # Write resource dictionary. # Possibly TODO: more general ExtGState (graphics state dictionaries) # ColorSpace Pattern Shading Properties - resources = { - "Font": self.fontObject, - "XObject": self.XObjectObject, - "ExtGState": self._extGStateObject, - "Pattern": self.hatchObject, - "Shading": self.gouraudObject, - "ProcSet": procsets, - } + resources = {'Font': self.fontObject, + 'XObject': self.XObjectObject, + 'ExtGState': self._extGStateObject, + 'Pattern': self.hatchObject, + 'Shading': self.gouraudObject, + 'ProcSet': procsets} self.writeObject(self.resourceObject, resources) fontNames = _api.deprecated("3.11")(property(lambda self: self._fontNames)) @@ -802,16 +770,14 @@ def __init__(self, filename, metadata=None): @property def dviFontInfo(self): d = {} - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file("pdftex.map")) + tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) for pdfname, dvifont in self._dviFontInfo.items(): psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( "No usable font file found for {} (TeX: {}); " - "the font may lack a Type-1 version".format( - psfont.psname, dvifont.texname - ) - ) + "the font may lack a Type-1 version" + .format(psfont.psname, dvifont.texname)) d[dvifont.texname] = types.SimpleNamespace( dvifont=dvifont, pdfname=pdfname, @@ -826,41 +792,38 @@ def newPage(self, width, height): self.endStream() self.width, self.height = width, height - contentObject = self.reserveObject("page contents") - annotsObject = self.reserveObject("annotations") - thePage = { - "Type": Name("Page"), - "Parent": self.pagesObject, - "Resources": self.resourceObject, - "MediaBox": [0, 0, 72 * width, 72 * height], - "Contents": contentObject, - "Annots": annotsObject, - } - pageObject = self.reserveObject("page") + contentObject = self.reserveObject('page contents') + annotsObject = self.reserveObject('annotations') + thePage = {'Type': Name('Page'), + 'Parent': self.pagesObject, + 'Resources': self.resourceObject, + 'MediaBox': [0, 0, 72 * width, 72 * height], + 'Contents': contentObject, + 'Annots': annotsObject, + } + pageObject = self.reserveObject('page') self.writeObject(pageObject, thePage) self.pageList.append(pageObject) self._annotations.append((annotsObject, self.pageAnnotations)) - self.beginStream( - contentObject.id, self.reserveObject("length of content stream") - ) + self.beginStream(contentObject.id, + self.reserveObject('length of content stream')) # Initialize the pdf graphics state to match the default Matplotlib # graphics context (colorspace and joinstyle). - self.output(Name("DeviceRGB"), Op.setcolorspace_stroke) - self.output(Name("DeviceRGB"), Op.setcolorspace_nonstroke) - self.output(GraphicsContextPdf.joinstyles["round"], Op.setlinejoin) + self.output(Name('DeviceRGB'), Op.setcolorspace_stroke) + self.output(Name('DeviceRGB'), Op.setcolorspace_nonstroke) + self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin) # Clear the list of annotations for the next page self.pageAnnotations = [] def newTextnote(self, text, positionRect=[-100, -100, 0, 0]): # Create a new annotation of type text - theNote = { - "Type": Name("Annot"), - "Subtype": Name("Text"), - "Contents": text, - "Rect": positionRect, - } + theNote = {'Type': Name('Annot'), + 'Subtype': Name('Text'), + 'Contents': text, + 'Rect': positionRect, + } self.pageAnnotations.append(theNote) @staticmethod @@ -871,12 +834,13 @@ def _get_subset_prefix(charset): The prefix is six uppercase letters followed by a plus sign; see PDF reference section 5.5.3 Font Subsets. """ - def toStr(n, base): if n < base: return string.ascii_uppercase[n] else: - return toStr(n // base, base) + string.ascii_uppercase[n % base] + return ( + toStr(n // base, base) + string.ascii_uppercase[n % base] + ) # encode to string using base 26 hashed = hash(charset) % ((sys.maxsize + 1) * 2) @@ -899,21 +863,23 @@ def finalize(self): self._write_soft_mask_groups() self.writeHatches() self.writeGouraudTriangles() - xobjects = {name: ob for image, name, ob in self._images.values()} + xobjects = { + name: ob for image, name, ob in self._images.values()} for tup in self.markers.values(): xobjects[tup[0]] = tup[1] for name, value in self.multi_byte_charprocs.items(): xobjects[name] = value - for name, path, trans, ob, join, cap, padding, filled, stroked in self.paths: + for name, path, trans, ob, join, cap, padding, filled, stroked \ + in self.paths: xobjects[name] = ob self.writeObject(self.XObjectObject, xobjects) self.writeImages() self.writeMarkers() self.writePathCollectionTemplates() - self.writeObject( - self.pagesObject, - {"Type": Name("Pages"), "Kids": self.pageList, "Count": len(self.pageList)}, - ) + self.writeObject(self.pagesObject, + {'Type': Name('Pages'), + 'Kids': self.pageList, + 'Count': len(self.pageList)}) self.writeInfoDict() # Finalize the file @@ -939,7 +905,7 @@ def write(self, data): def output(self, *data): self.write(_fill([pdfRepr(x) for x in data])) - self.write(b"\n") + self.write(b'\n') def beginStream(self, id, len, extra=None, png=None): assert self.currentstream is None @@ -968,9 +934,9 @@ def fontName(self, fontprop): if isinstance(fontprop, str): filenames = [fontprop] - elif mpl.rcParams["pdf.use14corefonts"]: + elif mpl.rcParams['pdf.use14corefonts']: filenames = _fontManager._find_fonts_by_props( - fontprop, fontext="afm", directory=RendererPdf._afm_font_dir + fontprop, fontext='afm', directory=RendererPdf._afm_font_dir ) else: filenames = _fontManager._find_fonts_by_props(fontprop) @@ -982,7 +948,7 @@ def fontName(self, fontprop): if Fx is None: Fx = next(self._internal_font_seq) self._fontNames[fname] = Fx - _log.debug("Assigning font %s = %r", Fx, fname) + _log.debug('Assigning font %s = %r', Fx, fname) if not first_Fx: first_Fx = Fx @@ -997,60 +963,56 @@ def dviFontName(self, dvifont): Register the font internally (in ``_dviFontInfo``) if not yet registered. """ pdfname = Name(f"F-{dvifont.texname.decode('ascii')}") - _log.debug("Assigning font %s = %s (dvi)", pdfname, dvifont.texname) + _log.debug('Assigning font %s = %s (dvi)', pdfname, dvifont.texname) self._dviFontInfo[pdfname] = dvifont return Name(pdfname) def writeFonts(self): fonts = {} for pdfname, dvifont in sorted(self._dviFontInfo.items()): - _log.debug("Embedding Type-1 font %s from dvi.", dvifont.texname) + _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) for filename in sorted(self._fontNames): Fx = self._fontNames[filename] - _log.debug("Embedding font %s.", filename) - if filename.endswith(".afm"): + _log.debug('Embedding font %s.', filename) + if filename.endswith('.afm'): # from pdf.use14corefonts - _log.debug("Writing AFM font.") + _log.debug('Writing AFM font.') fonts[Fx] = self._write_afm_font(filename) else: # a normal TrueType font - _log.debug("Writing TrueType font.") + _log.debug('Writing TrueType font.') chars = self._character_tracker.used.get(filename) if chars: fonts[Fx] = self.embedTTF(filename, chars) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): - with open(filename, "rb") as fh: + with open(filename, 'rb') as fh: font = AFM(fh) fontname = font.get_fontname() - fontdict = { - "Type": Name("Font"), - "Subtype": Name("Type1"), - "BaseFont": Name(fontname), - "Encoding": Name("WinAnsiEncoding"), - } - fontdictObject = self.reserveObject("font dictionary") + fontdict = {'Type': Name('Font'), + 'Subtype': Name('Type1'), + 'BaseFont': Name(fontname), + 'Encoding': Name('WinAnsiEncoding')} + fontdictObject = self.reserveObject('font dictionary') self.writeObject(fontdictObject, fontdict) return fontdictObject def _embedTeXFont(self, dvifont): - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file("pdftex.map")) + tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( "No usable font file found for {} (TeX: {}); " - "the font may lack a Type-1 version".format( - psfont.psname, dvifont.texname - ) - ) + "the font may lack a Type-1 version" + .format(psfont.psname, dvifont.texname)) # The font dictionary is the top-level object describing a font - fontdictObject = self.reserveObject("font dictionary") + fontdictObject = self.reserveObject('font dictionary') fontdict = { - "Type": Name("Font"), - "Subtype": Name("Type1"), + 'Type': Name('Font'), + 'Subtype': Name('Type1'), } # Read the font file and apply any encoding changes and effects @@ -1066,24 +1028,19 @@ def _embedTeXFont(self, dvifont): # for that subset, and compute various properties based on the encoding. chars = frozenset(self._character_tracker.used[dvifont.fname]) t1font = t1font.subset(chars, self._get_subset_prefix(chars)) - fontdict["BaseFont"] = Name(t1font.prop["FontName"]) + fontdict['BaseFont'] = Name(t1font.prop['FontName']) # createType1Descriptor writes the font data as a side effect - fontdict["FontDescriptor"] = self.createType1Descriptor(t1font) - encoding = t1font.prop["Encoding"] - fontdict["Encoding"] = self._generate_encoding(encoding) - fc = fontdict["FirstChar"] = min(encoding.keys(), default=0) - lc = fontdict["LastChar"] = max(encoding.keys(), default=255) + fontdict['FontDescriptor'] = self.createType1Descriptor(t1font) + encoding = t1font.prop['Encoding'] + fontdict['Encoding'] = self._generate_encoding(encoding) + fc = fontdict['FirstChar'] = min(encoding.keys(), default=0) + lc = fontdict['LastChar'] = max(encoding.keys(), default=255) # Convert glyph widths from TeX 12.20 fixed point to 1/1000 text space units font_metrics = dvifont._metrics - widths = [ - ( - (1000 * glyph_metrics.tex_width) >> 20 - if (glyph_metrics := font_metrics.get_metrics(char)) - else 0 - ) - for char in range(fc, lc + 1) - ] - fontdict["Widths"] = widthsObject = self.reserveObject("glyph widths") + widths = [(1000 * glyph_metrics.tex_width) >> 20 + if (glyph_metrics := font_metrics.get_metrics(char)) else 0 + for char in range(fc, lc + 1)] + fontdict['Widths'] = widthsObject = self.reserveObject('glyph widths') self.writeObject(widthsObject, widths) self.writeObject(fontdictObject, fontdict) return fontdictObject @@ -1096,17 +1053,20 @@ def _generate_encoding(self, encoding): result.append(code) prev = code result.append(Name(name)) - return {"Type": Name("Encoding"), "Differences": result} + return { + 'Type': Name('Encoding'), + 'Differences': result + } @_api.delete_parameter("3.11", "fontfile") def createType1Descriptor(self, t1font, fontfile=None): # Create and write the font descriptor and the font file # of a Type-1 font - fontdescObject = self.reserveObject("font descriptor") - fontfileObject = self.reserveObject("font file") + fontdescObject = self.reserveObject('font descriptor') + fontfileObject = self.reserveObject('font file') - italic_angle = t1font.prop["ItalicAngle"] - fixed_pitch = t1font.prop["isFixedPitch"] + italic_angle = t1font.prop['ItalicAngle'] + fixed_pitch = t1font.prop['isFixedPitch'] flags = 0 # fixed width @@ -1134,50 +1094,47 @@ def createType1Descriptor(self, t1font, fontfile=None): if 0: flags |= 1 << 18 - encoding = t1font.prop["Encoding"] - charset = "".join(sorted(f"/{c}" for c in encoding.values() if c != ".notdef")) + encoding = t1font.prop['Encoding'] + charset = ''.join( + sorted( + f'/{c}' for c in encoding.values() + if c != '.notdef' + ) + ) descriptor = { - "Type": Name("FontDescriptor"), - "FontName": Name(t1font.prop["FontName"]), - "Flags": flags, - "FontBBox": t1font.prop["FontBBox"], - "ItalicAngle": italic_angle, - "Ascent": t1font.prop["FontBBox"][3], - "Descent": t1font.prop["FontBBox"][1], - "CapHeight": 1000, # TODO: find this out - "XHeight": 500, # TODO: this one too - "FontFile": fontfileObject, - "FontFamily": t1font.prop["FamilyName"], - "StemV": 50, # TODO - "CharSet": charset, + 'Type': Name('FontDescriptor'), + 'FontName': Name(t1font.prop['FontName']), + 'Flags': flags, + 'FontBBox': t1font.prop['FontBBox'], + 'ItalicAngle': italic_angle, + 'Ascent': t1font.prop['FontBBox'][3], + 'Descent': t1font.prop['FontBBox'][1], + 'CapHeight': 1000, # TODO: find this out + 'XHeight': 500, # TODO: this one too + 'FontFile': fontfileObject, + 'FontFamily': t1font.prop['FamilyName'], + 'StemV': 50, # TODO + 'CharSet': charset, # (see also revision 3874; but not all TeX distros have AFM files!) # 'FontWeight': a number where 400 = Regular, 700 = Bold } self.writeObject(fontdescObject, descriptor) - self.outputStream( - fontfileObject, - b"".join(t1font.parts[:2]), - extra={ - "Length1": len(t1font.parts[0]), - "Length2": len(t1font.parts[1]), - "Length3": 0, - }, - ) + self.outputStream(fontfileObject, b"".join(t1font.parts[:2]), + extra={'Length1': len(t1font.parts[0]), + 'Length2': len(t1font.parts[1]), + 'Length3': 0}) return fontdescObject def _get_xobject_glyph_name(self, filename, glyph_name): Fx = self.fontName(filename) - return "-".join( - [ - Fx.name.decode(), - os.path.splitext(os.path.basename(filename))[0], - glyph_name, - ] - ) + return "-".join([ + Fx.name.decode(), + os.path.splitext(os.path.basename(filename))[0], + glyph_name]) _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin @@ -1204,7 +1161,7 @@ def embedTTF(self, filename, characters): """Embed the TTF font from the named file into the document.""" font = get_font(filename) - fonttype = mpl.rcParams["pdf.fonttype"] + fonttype = mpl.rcParams['pdf.fonttype'] def cvt(length, upe=font.units_per_EM, nearest=True): """Convert font coordinates to PDF glyph coordinates.""" @@ -1219,28 +1176,30 @@ def cvt(length, upe=font.units_per_EM, nearest=True): def embedTTFType3(font, characters, descriptor): """The Type 3-specific part of embedding a Truetype font""" - widthsObject = self.reserveObject("font widths") - fontdescObject = self.reserveObject("font descriptor") - fontdictObject = self.reserveObject("font dictionary") - charprocsObject = self.reserveObject("character procs") + widthsObject = self.reserveObject('font widths') + fontdescObject = self.reserveObject('font descriptor') + fontdictObject = self.reserveObject('font dictionary') + charprocsObject = self.reserveObject('character procs') differencesArray = [] firstchar, lastchar = 0, 255 bbox = [cvt(x, nearest=False) for x in font.bbox] fontdict = { - "Type": Name("Font"), - "BaseFont": ps_name, - "FirstChar": firstchar, - "LastChar": lastchar, - "FontDescriptor": fontdescObject, - "Subtype": Name("Type3"), - "Name": descriptor["FontName"], - "FontBBox": bbox, - "FontMatrix": [0.001, 0, 0, 0.001, 0, 0], - "CharProcs": charprocsObject, - "Encoding": {"Type": Name("Encoding"), "Differences": differencesArray}, - "Widths": widthsObject, - } + 'Type': Name('Font'), + 'BaseFont': ps_name, + 'FirstChar': firstchar, + 'LastChar': lastchar, + 'FontDescriptor': fontdescObject, + 'Subtype': Name('Type3'), + 'Name': descriptor['FontName'], + 'FontBBox': bbox, + 'FontMatrix': [.001, 0, 0, .001, 0, 0], + 'CharProcs': charprocsObject, + 'Encoding': { + 'Type': Name('Encoding'), + 'Differences': differencesArray}, + 'Widths': widthsObject + } from encodings import cp1252 @@ -1248,20 +1207,16 @@ def embedTTFType3(font, characters, descriptor): def get_char_width(charcode): s = ord(cp1252.decoding_table[charcode]) width = font.load_char( - s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING - ).horiAdvance + s, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING).horiAdvance return cvt(width) - with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning # from ft2font: here we're just building the widths table, but # the missing glyphs may not even be used in the actual string. warnings.filterwarnings("ignore") - widths = [ - get_char_width(charcode) - for charcode in range(firstchar, lastchar + 1) - ] - descriptor["MaxWidth"] = max(widths) + widths = [get_char_width(charcode) + for charcode in range(firstchar, lastchar+1)] + descriptor['MaxWidth'] = max(widths) # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids @@ -1296,19 +1251,17 @@ def get_char_width(charcode): # The 2-byte characters are used as XObjects, so they # need extra info in their dictionary if charname in multi_byte_chars: - charprocDict = { - "Type": Name("XObject"), - "Subtype": Name("Form"), - "BBox": bbox, - } + charprocDict = {'Type': Name('XObject'), + 'Subtype': Name('Form'), + 'BBox': bbox} # Each glyph includes bounding box information, # but xpdf and ghostscript can't handle it in a # Form XObject (they segfault!!!), so we remove it # from the stream here. It's not needed anyway, # since the Form XObject includes it in its BBox # value. - stream = stream[stream.find(b"d1") + 2 :] - charprocObject = self.reserveObject("charProc") + stream = stream[stream.find(b"d1") + 2:] + charprocObject = self.reserveObject('charProc') self.outputStream(charprocObject, stream, extra=charprocDict) # Send the glyphs with ccode > 255 to the XObject dictionary, @@ -1329,23 +1282,21 @@ def get_char_width(charcode): def embedTTFType42(font, characters, descriptor): """The Type 42-specific part of embedding a Truetype font""" - fontdescObject = self.reserveObject("font descriptor") - cidFontDictObject = self.reserveObject("CID font dictionary") - type0FontDictObject = self.reserveObject("Type 0 font dictionary") - cidToGidMapObject = self.reserveObject("CIDToGIDMap stream") - fontfileObject = self.reserveObject("font file stream") - wObject = self.reserveObject("Type 0 widths") - toUnicodeMapObject = self.reserveObject("ToUnicode map") + fontdescObject = self.reserveObject('font descriptor') + cidFontDictObject = self.reserveObject('CID font dictionary') + type0FontDictObject = self.reserveObject('Type 0 font dictionary') + cidToGidMapObject = self.reserveObject('CIDToGIDMap stream') + fontfileObject = self.reserveObject('font file stream') + wObject = self.reserveObject('Type 0 widths') + toUnicodeMapObject = self.reserveObject('ToUnicode map') subset_str = "".join(chr(c) for c in characters) _log.debug("SUBSET %s characters: %s", filename, subset_str) with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s %d -> %d", - filename, - os.stat(filename).st_size, - fontdata.getbuffer().nbytes, + "SUBSET %s %d -> %d", filename, + os.stat(filename).st_size, fontdata.getbuffer().nbytes ) # We need this ref for XObjects @@ -1357,53 +1308,49 @@ def embedTTFType42(font, characters, descriptor): font = FT2Font(fontdata) cidFontDict = { - "Type": Name("Font"), - "Subtype": Name("CIDFontType2"), - "BaseFont": ps_name, - "CIDSystemInfo": { - "Registry": "Adobe", - "Ordering": "Identity", - "Supplement": 0, - }, - "FontDescriptor": fontdescObject, - "W": wObject, - "CIDToGIDMap": cidToGidMapObject, - } + 'Type': Name('Font'), + 'Subtype': Name('CIDFontType2'), + 'BaseFont': ps_name, + 'CIDSystemInfo': { + 'Registry': 'Adobe', + 'Ordering': 'Identity', + 'Supplement': 0}, + 'FontDescriptor': fontdescObject, + 'W': wObject, + 'CIDToGIDMap': cidToGidMapObject + } type0FontDict = { - "Type": Name("Font"), - "Subtype": Name("Type0"), - "BaseFont": ps_name, - "Encoding": Name("Identity-H"), - "DescendantFonts": [cidFontDictObject], - "ToUnicode": toUnicodeMapObject, - } + 'Type': Name('Font'), + 'Subtype': Name('Type0'), + 'BaseFont': ps_name, + 'Encoding': Name('Identity-H'), + 'DescendantFonts': [cidFontDictObject], + 'ToUnicode': toUnicodeMapObject + } # Make fontfile stream - descriptor["FontFile2"] = fontfileObject + descriptor['FontFile2'] = fontfileObject self.outputStream( - fontfileObject, - fontdata.getvalue(), - extra={"Length1": fontdata.getbuffer().nbytes}, - ) + fontfileObject, fontdata.getvalue(), + extra={'Length1': fontdata.getbuffer().nbytes}) # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap # at the same time - cid_to_gid_map = ["\0"] * 65536 + cid_to_gid_map = ['\0'] * 65536 widths = [] max_ccode = 0 for c in characters: ccode = c gind = font.get_char_index(ccode) - glyph = font.load_char( - ccode, flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING - ) + glyph = font.load_char(ccode, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) max_ccode = max(ccode, max_ccode) widths.sort() - cid_to_gid_map = cid_to_gid_map[: max_ccode + 1] + cid_to_gid_map = cid_to_gid_map[:max_ccode + 1] last_ccode = -2 w = [] @@ -1428,17 +1375,11 @@ def embedTTFType42(font, characters, descriptor): end = min(65535, end) unicode_bfrange.append( - b"<%04x> <%04x> [%s]" - % ( - start, - end, - b" ".join(b"<%04x>" % x for x in range(start, end + 1)), - ) - ) - unicode_cmap = self._identityToUnicodeCMap % ( - len(unicode_groups), - b"\n".join(unicode_bfrange), - ) + b"<%04x> <%04x> [%s]" % + (start, end, + b" ".join(b"<%04x>" % x for x in range(start, end+1)))) + unicode_cmap = (self._identityToUnicodeCMap % + (len(unicode_groups), b"\n".join(unicode_bfrange))) # Add XObjects for unsupported chars glyph_ids = [] @@ -1451,19 +1392,17 @@ def embedTTFType42(font, characters, descriptor): rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] - charprocDict = { - "Type": Name("XObject"), - "Subtype": Name("Form"), - "BBox": bbox, - } + charprocDict = {'Type': Name('XObject'), + 'Subtype': Name('Form'), + 'BBox': bbox} # Each glyph includes bounding box information, # but xpdf and ghostscript can't handle it in a # Form XObject (they segfault!!!), so we remove it # from the stream here. It's not needed anyway, # since the Form XObject includes it in its BBox # value. - stream = stream[stream.find(b"d1") + 2 :] - charprocObject = self.reserveObject("charProc") + stream = stream[stream.find(b"d1") + 2:] + charprocObject = self.reserveObject('charProc') self.outputStream(charprocObject, stream, extra=charprocDict) name = self._get_xobject_glyph_name(filename, charname) @@ -1476,7 +1415,7 @@ def embedTTFType42(font, characters, descriptor): # ToUnicode CMap self.outputStream(toUnicodeMapObject, unicode_cmap) - descriptor["MaxWidth"] = max_width + descriptor['MaxWidth'] = max_width # Write everything out self.writeObject(cidFontDictObject, cidFontDict) @@ -1488,11 +1427,14 @@ def embedTTFType42(font, characters, descriptor): # Beginning of main embedTTF function... - ps_name = self._get_subsetted_psname(font.postscript_name, font.get_charmap()) - ps_name = ps_name.encode("ascii", "replace") + ps_name = self._get_subsetted_psname( + font.postscript_name, + font.get_charmap() + ) + ps_name = ps_name.encode('ascii', 'replace') ps_name = Name(ps_name) - pclt = font.get_sfnt_table("pclt") or {"capHeight": 0, "xHeight": 0} - post = font.get_sfnt_table("post") or {"italicAngle": (0, 0)} + pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} + post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)} ff = font.face_flags sf = font.style_flags @@ -1516,17 +1458,17 @@ def embedTTFType42(font, characters, descriptor): flags |= 1 << 18 descriptor = { - "Type": Name("FontDescriptor"), - "FontName": ps_name, - "Flags": flags, - "FontBBox": [cvt(x, nearest=False) for x in font.bbox], - "Ascent": cvt(font.ascender, nearest=False), - "Descent": cvt(font.descender, nearest=False), - "CapHeight": cvt(pclt["capHeight"], nearest=False), - "XHeight": cvt(pclt["xHeight"]), - "ItalicAngle": post["italicAngle"][1], # ??? - "StemV": 0, # ??? - } + 'Type': Name('FontDescriptor'), + 'FontName': ps_name, + 'Flags': flags, + 'FontBBox': [cvt(x, nearest=False) for x in font.bbox], + 'Ascent': cvt(font.ascender, nearest=False), + 'Descent': cvt(font.descender, nearest=False), + 'CapHeight': cvt(pclt['capHeight'], nearest=False), + 'XHeight': cvt(pclt['xHeight']), + 'ItalicAngle': post['italicAngle'][1], # ??? + 'StemV': 0 # ??? + } if fonttype == 3: return embedTTFType3(font, characters, descriptor) @@ -1541,10 +1483,9 @@ def alphaState(self, alpha): return state[0] name = next(self._alpha_state_seq) - self.alphaStates[alpha] = ( - name, - {"Type": Name("ExtGState"), "CA": alpha[0], "ca": alpha[1]}, - ) + self.alphaStates[alpha] = \ + (name, {'Type': Name('ExtGState'), + 'CA': alpha[0], 'ca': alpha[1]}) return name def _soft_mask_state(self, smask): @@ -1567,41 +1508,45 @@ def _soft_mask_state(self, smask): return state[0] name = next(self._soft_mask_seq) - groupOb = self.reserveObject("transparency group for soft mask") + groupOb = self.reserveObject('transparency group for soft mask') self._soft_mask_states[smask] = ( name, { - "Type": Name("ExtGState"), - "AIS": False, - "SMask": { - "Type": Name("Mask"), - "S": Name("Luminosity"), - "BC": [1], - "G": groupOb, - }, - }, + 'Type': Name('ExtGState'), + 'AIS': False, + 'SMask': { + 'Type': Name('Mask'), + 'S': Name('Luminosity'), + 'BC': [1], + 'G': groupOb + } + } ) - self._soft_mask_groups.append( - ( - groupOb, - { - "Type": Name("XObject"), - "Subtype": Name("Form"), - "FormType": 1, - "Group": {"S": Name("Transparency"), "CS": Name("DeviceGray")}, - "Matrix": [1, 0, 0, 1, 0, 0], - "Resources": {"Shading": {"S": smask}}, - "BBox": [0, 0, 1, 1], + self._soft_mask_groups.append(( + groupOb, + { + 'Type': Name('XObject'), + 'Subtype': Name('Form'), + 'FormType': 1, + 'Group': { + 'S': Name('Transparency'), + 'CS': Name('DeviceGray') }, - [Name("S"), Op.shading], - ) - ) + 'Matrix': [1, 0, 0, 1, 0, 0], + 'Resources': {'Shading': {'S': smask}}, + 'BBox': [0, 0, 1, 1] + }, + [Name('S'), Op.shading] + )) return name def writeExtGSTates(self): self.writeObject( self._extGStateObject, - dict([*self.alphaStates.values(), *self._soft_mask_states.values()]), + dict([ + *self.alphaStates.values(), + *self._soft_mask_states.values() + ]) ) def _write_soft_mask_groups(self): @@ -1627,66 +1572,45 @@ def hatchPattern(self, hatch_style): self._hatch_patterns[hatch_style] = name return name - hatchPatterns = _api.deprecated("3.10")( - property( - lambda self: { - k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() - } - ) - ) + hatchPatterns = _api.deprecated("3.10")(property(lambda self: { + k: (e, f, h) for k, (e, f, h, l) in self._hatch_patterns.items() + })) def writeHatches(self): hatchDict = dict() sidelen = 72.0 for hatch_style, name in self._hatch_patterns.items(): - ob = self.reserveObject("hatch pattern") + ob = self.reserveObject('hatch pattern') hatchDict[name] = ob - res = { - "Procsets": [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()] - } + res = {'Procsets': + [Name(x) for x in "PDF Text ImageB ImageC ImageI".split()]} self.beginStream( - ob.id, - None, - { - "Type": Name("Pattern"), - "PatternType": 1, - "PaintType": 1, - "TilingType": 1, - "BBox": [0, 0, sidelen, sidelen], - "XStep": sidelen, - "YStep": sidelen, - "Resources": res, - # Change origin to match Agg at top-left. - "Matrix": [1, 0, 0, 1, 0, self.height * 72], - }, - ) + ob.id, None, + {'Type': Name('Pattern'), + 'PatternType': 1, 'PaintType': 1, 'TilingType': 1, + 'BBox': [0, 0, sidelen, sidelen], + 'XStep': sidelen, 'YStep': sidelen, + 'Resources': res, + # Change origin to match Agg at top-left. + 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) stroke_rgb, fill_rgb, hatch, lw = hatch_style - self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_stroke) + self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], + Op.setrgb_stroke) if fill_rgb is not None: - self.output( - fill_rgb[0], - fill_rgb[1], - fill_rgb[2], - Op.setrgb_nonstroke, - 0, - 0, - sidelen, - sidelen, - Op.rectangle, - Op.fill, - ) - self.output( - stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_nonstroke - ) + self.output(fill_rgb[0], fill_rgb[1], fill_rgb[2], + Op.setrgb_nonstroke, + 0, 0, sidelen, sidelen, Op.rectangle, + Op.fill) + self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], + Op.setrgb_nonstroke) self.output(lw, Op.setlinewidth) - self.output( - *self.pathOperations( - Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False - ) - ) + self.output(*self.pathOperations( + Path.hatch(hatch), + Affine2D().scale(sidelen), + simplify=False)) self.output(Op.fill_stroke) self.endStream() @@ -1710,8 +1634,8 @@ def addGouraudTriangles(self, points, colors): ------- Name, Reference """ - name = Name("GT%d" % len(self.gouraudTriangles)) - ob = self.reserveObject(f"Gouraud triangle {name}") + name = Name('GT%d' % len(self.gouraudTriangles)) + ob = self.reserveObject(f'Gouraud triangle {name}') self.gouraudTriangles.append((name, ob, points, colors)) return name, ob @@ -1729,36 +1653,31 @@ def writeGouraudTriangles(self): colordim = 3 points_min = np.min(flat_points, axis=0) - (1 << 8) points_max = np.max(flat_points, axis=0) + (1 << 8) - factor = 0xFFFFFFFF / (points_max - points_min) + factor = 0xffffffff / (points_max - points_min) self.beginStream( - ob.id, - None, - { - "ShadingType": 4, - "BitsPerCoordinate": 32, - "BitsPerComponent": 8, - "BitsPerFlag": 8, - "ColorSpace": Name("DeviceRGB" if colordim == 3 else "DeviceGray"), - "AntiAlias": False, - "Decode": ( - [points_min[0], points_max[0], points_min[1], points_max[1]] - + [0, 1] * colordim - ), - }, - ) + ob.id, None, + {'ShadingType': 4, + 'BitsPerCoordinate': 32, + 'BitsPerComponent': 8, + 'BitsPerFlag': 8, + 'ColorSpace': Name( + 'DeviceRGB' if colordim == 3 else 'DeviceGray' + ), + 'AntiAlias': False, + 'Decode': ([points_min[0], points_max[0], + points_min[1], points_max[1]] + + [0, 1] * colordim), + }) streamarr = np.empty( (shape[0] * shape[1],), - dtype=[ - ("flags", "u1"), - ("points", ">u4", (2,)), - ("colors", "u1", (colordim,)), - ], - ) - streamarr["flags"] = 0 - streamarr["points"] = (flat_points - points_min) * factor - streamarr["colors"] = flat_colors[:, :colordim] * 255.0 + dtype=[('flags', 'u1'), + ('points', '>u4', (2,)), + ('colors', 'u1', (colordim,))]) + streamarr['flags'] = 0 + streamarr['points'] = (flat_points - points_min) * factor + streamarr['colors'] = flat_colors[:, :colordim] * 255.0 self.write(streamarr.tobytes()) self.endStream() @@ -1772,7 +1691,7 @@ def imageObject(self, image): return entry[1] name = next(self._image_seq) - ob = self.reserveObject(f"image {name}") + ob = self.reserveObject(f'image {name}') self._images[id(image)] = (image, name, ob) return name @@ -1787,14 +1706,14 @@ def _unpack(self, im): return im, None else: rgb = im[:, :, :3] - rgb = np.array(rgb, order="C") + rgb = np.array(rgb, order='C') # PDF needs a separate alpha image if im.shape[2] == 4: alpha = im[:, :, 3][..., None] if np.all(alpha == 255): alpha = None else: - alpha = np.array(alpha, order="C") + alpha = np.array(alpha, order='C') else: alpha = None return rgb, alpha @@ -1807,25 +1726,25 @@ def _writePng(self, img): buffer = BytesIO() img.save(buffer, format="png") buffer.seek(8) - png_data = b"" + png_data = b'' bit_depth = palette = None while True: - length, type = struct.unpack(b"!L4s", buffer.read(8)) - if type in [b"IHDR", b"PLTE", b"IDAT"]: + length, type = struct.unpack(b'!L4s', buffer.read(8)) + if type in [b'IHDR', b'PLTE', b'IDAT']: data = buffer.read(length) if len(data) != length: raise RuntimeError("truncated data") - if type == b"IHDR": + if type == b'IHDR': bit_depth = int(data[8]) - elif type == b"PLTE": + elif type == b'PLTE': palette = data - elif type == b"IDAT": + elif type == b'IDAT': png_data += data - elif type == b"IEND": + elif type == b'IEND': break else: buffer.seek(length, 1) - buffer.seek(4, 1) # skip CRC + buffer.seek(4, 1) # skip CRC return png_data, bit_depth, palette def _writeImg(self, data, id, smask=None): @@ -1836,39 +1755,32 @@ def _writeImg(self, data, id, smask=None): width, 1)`` array. """ height, width, color_channels = data.shape - obj = { - "Type": Name("XObject"), - "Subtype": Name("Image"), - "Width": width, - "Height": height, - "ColorSpace": Name({1: "DeviceGray", 3: "DeviceRGB"}[color_channels]), - "BitsPerComponent": 8, - } + obj = {'Type': Name('XObject'), + 'Subtype': Name('Image'), + 'Width': width, + 'Height': height, + 'ColorSpace': Name({1: 'DeviceGray', 3: 'DeviceRGB'}[color_channels]), + 'BitsPerComponent': 8} if smask: - obj["SMask"] = smask - if mpl.rcParams["pdf.compression"]: + obj['SMask'] = smask + if mpl.rcParams['pdf.compression']: if data.shape[-1] == 1: data = data.squeeze(axis=-1) - png = {"Predictor": 10, "Colors": color_channels, "Columns": width} + png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width} img = Image.fromarray(data) img_colors = img.getcolors(maxcolors=256) if color_channels == 3 and img_colors is not None: # Convert to indexed color if there are 256 colors or fewer. This can # significantly reduce the file size. num_colors = len(img_colors) - palette = np.array( - [comp for _, color in img_colors for comp in color], dtype=np.uint8 - ) - palette24 = ( - (palette[0::3].astype(np.uint32) << 16) - | (palette[1::3].astype(np.uint32) << 8) - | palette[2::3] - ) - rgb24 = ( - (data[:, :, 0].astype(np.uint32) << 16) - | (data[:, :, 1].astype(np.uint32) << 8) - | data[:, :, 2] - ) + palette = np.array([comp for _, color in img_colors for comp in color], + dtype=np.uint8) + palette24 = ((palette[0::3].astype(np.uint32) << 16) | + (palette[1::3].astype(np.uint32) << 8) | + palette[2::3]) + rgb24 = ((data[:, :, 0].astype(np.uint32) << 16) | + (data[:, :, 1].astype(np.uint32) << 8) | + data[:, :, 2]) indices = np.argsort(palette24).astype(np.uint8) rgb8 = indices[np.searchsorted(palette24, rgb24, sorter=indices)] img = Image.fromarray(rgb8).convert("P") @@ -1876,23 +1788,22 @@ def _writeImg(self, data, id, smask=None): png_data, bit_depth, palette = self._writePng(img) if bit_depth is None or palette is None: raise RuntimeError("invalid PNG header") - palette = palette[ - : num_colors * 3 - ] # Trim padding; remove for Pillow>=9 - obj["ColorSpace"] = [ - Name("Indexed"), - Name("DeviceRGB"), - num_colors - 1, - palette, - ] - obj["BitsPerComponent"] = bit_depth - png["Colors"] = 1 - png["BitsPerComponent"] = bit_depth + palette = palette[:num_colors * 3] # Trim padding; remove for Pillow>=9 + obj['ColorSpace'] = [Name('Indexed'), Name('DeviceRGB'), + num_colors - 1, palette] + obj['BitsPerComponent'] = bit_depth + png['Colors'] = 1 + png['BitsPerComponent'] = bit_depth else: png_data, _, _ = self._writePng(img) else: png = None - self.beginStream(id, self.reserveObject("length of image stream"), obj, png=png) + self.beginStream( + id, + self.reserveObject('length of image stream'), + obj, + png=png + ) if png: self.currentstream.write(png_data) else: @@ -1909,7 +1820,8 @@ def writeImages(self): smaskObject = None self._writeImg(data, ob.id, smaskObject) - def markerObject(self, path, trans, fill, stroke, lw, joinstyle, capstyle): + def markerObject(self, path, trans, fill, stroke, lw, joinstyle, + capstyle): """Return name of a marker XObject representing the given path.""" # self.markers used by markerObject, writeMarkers, close: # mapping from (path operations, fill?, stroke?) to @@ -1927,8 +1839,8 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle, capstyle): key = (tuple(pathops), bool(fill), bool(stroke), joinstyle, capstyle) result = self.markers.get(key) if result is None: - name = Name("M%d" % len(self.markers)) - ob = self.reserveObject("marker %d" % len(self.markers)) + name = Name('M%d' % len(self.markers)) + ob = self.reserveObject('marker %d' % len(self.markers)) bbox = path.get_extents(trans) self.markers[key] = [name, ob, bbox, lw] else: @@ -1938,12 +1850,8 @@ def markerObject(self, path, trans, fill, stroke, lw, joinstyle, capstyle): return name def writeMarkers(self): - for (pathops, fill, stroke, joinstyle, capstyle), ( - name, - ob, - bbox, - lw, - ) in self.markers.items(): + for ((pathops, fill, stroke, joinstyle, capstyle), + (name, ob, bbox, lw)) in self.markers.items(): # bbox wraps the exact limits of the control points, so half a line # will appear outside it. If the join style is miter and the line # is not parallel to the edge, then the line will extend even @@ -1953,51 +1861,28 @@ def writeMarkers(self): # following padding: bbox = bbox.padded(lw * 5) self.beginStream( - ob.id, - None, - { - "Type": Name("XObject"), - "Subtype": Name("Form"), - "BBox": list(bbox.extents), - }, - ) - self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin) + ob.id, None, + {'Type': Name('XObject'), 'Subtype': Name('Form'), + 'BBox': list(bbox.extents)}) + self.output(GraphicsContextPdf.joinstyles[joinstyle], + Op.setlinejoin) self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) self.output(*pathops) self.output(Op.paint_path(fill, stroke)) self.endStream() def pathCollectionObject(self, gc, path, trans, padding, filled, stroked): - name = Name("P%d" % len(self.paths)) - ob = self.reserveObject("path %d" % len(self.paths)) + name = Name('P%d' % len(self.paths)) + ob = self.reserveObject('path %d' % len(self.paths)) self.paths.append( - ( - name, - path, - trans, - ob, - gc.get_joinstyle(), - gc.get_capstyle(), - padding, - filled, - stroked, - ) - ) + (name, path, trans, ob, gc.get_joinstyle(), gc.get_capstyle(), + padding, filled, stroked)) return name def writePathCollectionTemplates(self): - for ( - name, - path, - trans, - ob, - joinstyle, - capstyle, - padding, - filled, - stroked, - ) in self.paths: - pathops = self.pathOperations(path, trans, simplify=path.should_simplify) + for (name, path, trans, ob, joinstyle, capstyle, padding, filled, + stroked) in self.paths: + pathops = self.pathOperations(path, trans, simplify=False) bbox = path.get_extents(trans) if not np.all(np.isfinite(bbox.extents)): extents = [0, 0, 0, 0] @@ -2005,11 +1890,11 @@ def writePathCollectionTemplates(self): bbox = bbox.padded(padding) extents = list(bbox.extents) self.beginStream( - ob.id, - None, - {"Type": Name("XObject"), "Subtype": Name("Form"), "BBox": extents}, - ) - self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin) + ob.id, None, + {'Type': Name('XObject'), 'Subtype': Name('Form'), + 'BBox': extents}) + self.output(GraphicsContextPdf.joinstyles[joinstyle], + Op.setlinejoin) self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) self.output(*pathops) self.output(Op.paint_path(filled, stroked)) @@ -2017,26 +1902,12 @@ def writePathCollectionTemplates(self): @staticmethod def pathOperations(path, transform, clip=None, simplify=None, sketch=None): - return [ - Verbatim( - _path.convert_to_string( - path, - transform, - clip, - simplify, - sketch, - 6, - [ - Op.moveto.value, - Op.lineto.value, - b"", - Op.curveto.value, - Op.closepath.value, - ], - True, - ) - ) - ] + return [Verbatim(_path.convert_to_string( + path, transform, clip, simplify, sketch, + 6, + [Op.moveto.value, Op.lineto.value, b'', Op.curveto.value, + Op.closepath.value], + True))] def writePath(self, path, transform, clip=False, sketch=None): if clip: @@ -2044,13 +1915,12 @@ def writePath(self, path, transform, clip=False, sketch=None): simplify = path.should_simplify else: clip = None - simplify = path.should_simplify - cmds = self.pathOperations( - path, transform, clip, simplify=simplify, sketch=sketch - ) + simplify = False + cmds = self.pathOperations(path, transform, clip, simplify=simplify, + sketch=sketch) self.output(*cmds) - def reserveObject(self, name=""): + def reserveObject(self, name=''): """ Reserve an ID for an indirect object. @@ -2074,31 +1944,27 @@ def writeXref(self): self.write(b"xref\n0 %d\n" % len(self.xrefTable)) for i, (offset, generation, name) in enumerate(self.xrefTable): if offset is None: - raise AssertionError("No offset for object %d (%s)" % (i, name)) + raise AssertionError( + 'No offset for object %d (%s)' % (i, name)) else: - key = b"f" if name == "the zero object" else b"n" + key = b"f" if name == 'the zero object' else b"n" text = b"%010d %05d %b \n" % (offset, generation, key) self.write(text) def writeInfoDict(self): """Write out the info dictionary, checking it for good form""" - self.infoObject = self.reserveObject("info") + self.infoObject = self.reserveObject('info') self.writeObject(self.infoObject, self.infoDict) def writeTrailer(self): """Write out the PDF trailer.""" self.write(b"trailer\n") - self.write( - pdfRepr( - { - "Size": len(self.xrefTable), - "Root": self.rootObject, - "Info": self.infoObject, - } - ) - ) + self.write(pdfRepr( + {'Size': len(self.xrefTable), + 'Root': self.rootObject, + 'Info': self.infoObject})) # Could add 'ID' self.write(b"\nstartxref\n%d\n%%%%EOF\n" % self.startxref) @@ -2118,10 +1984,10 @@ def finalize(self): self.file.output(*self.gc.finalize()) def check_gc(self, gc, fillcolor=None): - orig_fill = getattr(gc, "_fillcolor", (0.0, 0.0, 0.0)) + orig_fill = getattr(gc, '_fillcolor', (0., 0., 0.)) gc._fillcolor = fillcolor - orig_alphas = getattr(gc, "_effective_alphas", (1.0, 1.0)) + orig_alphas = getattr(gc, '_effective_alphas', (1.0, 1.0)) if gc.get_rgb() is None: # It should not matter what color here since linewidth should be @@ -2145,7 +2011,7 @@ def check_gc(self, gc, fillcolor=None): gc._effective_alphas = orig_alphas def get_image_magnification(self): - return self.image_dpi / 72.0 + return self.image_dpi/72.0 def draw_image(self, gc, x, y, im, transform=None): # docstring inherited @@ -2166,72 +2032,30 @@ def draw_image(self, gc, x, y, im, transform=None): imob = self.file.imageObject(im) if transform is None: - self.file.output( - Op.gsave, - w, - 0, - 0, - h, - x, - y, - Op.concat_matrix, - imob, - Op.use_xobject, - Op.grestore, - ) + self.file.output(Op.gsave, + w, 0, 0, h, x, y, Op.concat_matrix, + imob, Op.use_xobject, Op.grestore) else: tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values() - self.file.output( - Op.gsave, - 1, - 0, - 0, - 1, - x, - y, - Op.concat_matrix, - tr1, - tr2, - tr3, - tr4, - tr5, - tr6, - Op.concat_matrix, - imob, - Op.use_xobject, - Op.grestore, - ) + self.file.output(Op.gsave, + 1, 0, 0, 1, x, y, Op.concat_matrix, + tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix, + imob, Op.use_xobject, Op.grestore) def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited self.check_gc(gc, rgbFace) self.file.writePath( - path, - transform, + path, transform, rgbFace is None and gc.get_hatch_path() is None, - gc.get_sketch_params(), - ) + gc.get_sketch_params()) self.file.output(self.gc.paint()) - def draw_path_collection( - self, - gc, - master_transform, - paths, - all_transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - *, - hatchcolors=None, - ): + def draw_path_collection(self, gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position, *, hatchcolors=None): # We can only reuse the objects if the presence of fill and # stroke (and the amount of alpha for each) is the same for # all of them @@ -2267,73 +2091,50 @@ def draw_path_collection( # uses_per_path for the uses 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 + uses_per_path + 5 < len_path * uses_per_path + paths, all_transforms, offsets, facecolors, edgecolors) + should_do_optimization = \ + len_path + uses_per_path + 5 < len_path * uses_per_path if (not can_do_optimization) or (not should_do_optimization): return RendererBase.draw_path_collection( - self, - gc, - master_transform, - paths, - all_transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - hatchcolors=hatchcolors, - ) + self, gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position, hatchcolors=hatchcolors) padding = np.max(linewidths) path_codes = [] - for i, (path, transform) in enumerate( - self._iter_collection_raw_paths(master_transform, paths, all_transforms) - ): + for i, (path, transform) in enumerate(self._iter_collection_raw_paths( + master_transform, paths, all_transforms)): name = self.file.pathCollectionObject( - gc, path, transform, padding, filled, stroked - ) + gc, path, transform, padding, filled, stroked) path_codes.append(name) output = self.file.output output(*self.gc.push()) lastx, lasty = 0, 0 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, - path_codes, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - hatchcolors=hatchcolors, - ): + gc, path_codes, offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): self.check_gc(gc0, rgbFace) dx, dy = xo - lastx, yo - lasty - output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, Op.use_xobject) + output(1, 0, 0, 1, dx, dy, Op.concat_matrix, path_id, + Op.use_xobject) lastx, lasty = xo, yo output(*self.gc.pop()) - def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): + def draw_markers(self, gc, marker_path, marker_trans, path, trans, + rgbFace=None): # docstring inherited # Same logic as in draw_path_collection len_marker_path = len(marker_path) uses = len(path) if len_marker_path * uses < len_marker_path + uses + 5: - RendererBase.draw_markers( - self, gc, marker_path, marker_trans, path, trans, rgbFace - ) + RendererBase.draw_markers(self, gc, marker_path, marker_trans, + path, trans, rgbFace) return self.check_gc(gc, rgbFace) @@ -2342,30 +2143,23 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) output = self.file.output marker = self.file.markerObject( - marker_path, - marker_trans, - fill, - stroke, - self.gc._linewidth, - gc.get_joinstyle(), - gc.get_capstyle(), - ) + marker_path, marker_trans, fill, stroke, self.gc._linewidth, + gc.get_joinstyle(), gc.get_capstyle()) output(Op.gsave) lastx, lasty = 0, 0 for vertices, code in path.iter_segments( - trans, - clip=(0, 0, self.file.width * 72, self.file.height * 72), - simplify=False, - ): + trans, + clip=(0, 0, self.file.width*72, self.file.height*72), + simplify=False): if len(vertices): x, y = vertices[-2:] - if not ( - 0 <= x <= self.file.width * 72 and 0 <= y <= self.file.height * 72 - ): + if not (0 <= x <= self.file.width * 72 + and 0 <= y <= self.file.height * 72): continue dx, dy = x - lastx, y - lasty - output(1, 0, 0, 1, dx, dy, Op.concat_matrix, marker, Op.use_xobject) + output(1, 0, 0, 1, dx, dy, Op.concat_matrix, + marker, Op.use_xobject) lastx, lasty = x, y output(Op.grestore) @@ -2405,43 +2199,37 @@ def draw_gouraud_triangles(self, gc, points, colors, trans): alpha = colors[:, :, 3][:, :, None] _, smask_ob = self.file.addGouraudTriangles(tpoints, alpha) gstate = self.file._soft_mask_state(smask_ob) - output(Op.gsave, gstate, Op.setgstate, name, Op.shading, Op.grestore) + output(Op.gsave, gstate, Op.setgstate, + name, Op.shading, + Op.grestore) def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0): if angle == oldangle == 0: self.file.output(x - oldx, y - oldy, Op.textpos) else: angle = math.radians(angle) - self.file.output( - math.cos(angle), - math.sin(angle), - -math.sin(angle), - math.cos(angle), - x, - y, - Op.textmatrix, - ) + self.file.output(math.cos(angle), math.sin(angle), + -math.sin(angle), math.cos(angle), + x, y, Op.textmatrix) self.file.output(0, 0, Op.textpos) def draw_mathtext(self, gc, x, y, s, prop, angle): # TODO: fix positioning and encoding - width, height, descent, glyphs, rects = self._text2path.mathtext_parser.parse( - s, 72, prop - ) + width, height, descent, glyphs, rects = \ + self._text2path.mathtext_parser.parse(s, 72, prop) if gc.get_url() is not None: - self.file._annotations[-1][1].append( - _get_link_annotation(gc, x, y, width, height, angle) - ) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, width, height, angle)) - fonttype = mpl.rcParams["pdf.fonttype"] + fonttype = mpl.rcParams['pdf.fonttype'] # Set up a global transformation matrix for the whole math expression a = math.radians(angle) self.file.output(Op.gsave) - self.file.output( - math.cos(a), math.sin(a), -math.sin(a), math.cos(a), x, y, Op.concat_matrix - ) + self.file.output(math.cos(a), math.sin(a), + -math.sin(a), math.cos(a), + x, y, Op.concat_matrix) self.check_gc(gc, gc._rgb) prev_font = None, None @@ -2460,21 +2248,21 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy if (fontname, fontsize) != prev_font: - self.file.output( - self.file.fontName(fontname), fontsize, Op.selectfont - ) + self.file.output(self.file.fontName(fontname), fontsize, + Op.selectfont) prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), Op.show) + self.file.output(self.encode_string(chr(num), fonttype), + Op.show) self.file.output(Op.end_text) for font, fontsize, ox, oy, num in unsupported_chars: - self._draw_xobject_glyph(font, fontsize, font.get_char_index(num), ox, oy) + self._draw_xobject_glyph( + font, fontsize, font.get_char_index(num), ox, oy) # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: - self.file.output( - Op.gsave, ox, oy, width, height, Op.rectangle, Op.fill, Op.grestore - ) + self.file.output(Op.gsave, ox, oy, width, height, + Op.rectangle, Op.fill, Op.grestore) # Pop off the global transformation self.file.output(Op.grestore) @@ -2485,12 +2273,11 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): fontsize = prop.get_size_in_points() dvifile = texmanager.make_dvi(s, fontsize) with dviread.Dvi(dvifile, 72) as dvi: - (page,) = dvi + page, = dvi if gc.get_url() is not None: - self.file._annotations[-1][1].append( - _get_link_annotation(gc, x, y, page.width, page.height, angle) - ) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, page.width, page.height, angle)) # Gather font information and do some setup for combining # characters into strings. The variable seq will contain a @@ -2505,28 +2292,28 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): for x1, y1, dvifont, glyph, width in page.text: if dvifont != oldfont: pdfname = self.file.dviFontName(dvifont) - seq += [["font", pdfname, dvifont.size]] + seq += [['font', pdfname, dvifont.size]] oldfont = dvifont - seq += [["text", x1, y1, [bytes([glyph])], x1 + width]] + seq += [['text', x1, y1, [bytes([glyph])], x1+width]] self.file._character_tracker.track(dvifont, chr(glyph)) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one # string (if any kerns would be less than 0.1 points). i, curx, fontsize = 0, 0, None - while i < len(seq) - 1: - elt, nxt = seq[i : i + 2] - if elt[0] == "font": + while i < len(seq)-1: + elt, nxt = seq[i:i+2] + if elt[0] == 'font': fontsize = elt[2] - elif elt[0] == nxt[0] == "text" and elt[2] == nxt[2]: + elif elt[0] == nxt[0] == 'text' and elt[2] == nxt[2]: offset = elt[4] - nxt[1] if abs(offset) < 0.1: elt[3][-1] += nxt[3][0] - elt[4] += nxt[4] - nxt[1] + elt[4] += nxt[4]-nxt[1] else: - elt[3] += [offset * 1000.0 / fontsize, nxt[3][0]] + elt[3] += [offset*1000.0/fontsize, nxt[3][0]] elt[4] = nxt[4] - del seq[i + 1] + del seq[i+1] continue i += 1 @@ -2538,9 +2325,9 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): self.file.output(Op.begin_text) curx, cury, oldx, oldy = 0, 0, 0, 0 for elt in seq: - if elt[0] == "font": + if elt[0] == 'font': self.file.output(elt[1], elt[2], Op.selectfont) - elif elt[0] == "text": + elif elt[0] == 'text': curx, cury = mytrans.transform((elt[1], elt[2])) self._setup_textpos(curx, cury, angle, oldx, oldy) oldx, oldy = curx, cury @@ -2557,18 +2344,17 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): boxgc = self.new_gc() boxgc.copy_properties(gc) boxgc.set_linewidth(0) - pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] + pathops = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.CLOSEPOLY] for x1, y1, h, w in page.boxes: - path = Path( - [[x1, y1], [x1 + w, y1], [x1 + w, y1 + h], [x1, y1 + h], [0, 0]], - pathops, - ) + path = Path([[x1, y1], [x1+w, y1], [x1+w, y1+h], [x1, y1+h], + [0, 0]], pathops) self.draw_path(boxgc, path, mytrans, gc._rgb) def encode_string(self, s, fonttype): if fonttype in (1, 3): - return s.encode("cp1252", "replace") - return s.encode("utf-16be", "replace") + return s.encode('cp1252', 'replace') + return s.encode('utf-16be', 'replace') def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -2581,29 +2367,28 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fontsize = prop.get_size_in_points() - if mpl.rcParams["pdf.use14corefonts"]: + if mpl.rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) fonttype = 1 else: font = self._get_font_ttf(prop) self.file._character_tracker.track(font, s) - fonttype = mpl.rcParams["pdf.fonttype"] + fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: font.set_text(s) width, height = font.get_width_height() - self.file._annotations[-1][1].append( - _get_link_annotation(gc, x, y, width / 64, height / 64, angle) - ) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, width / 64, height / 64, angle)) # If fonttype is neither 3 nor 42, emit the whole string at once # without manual kerning. if fonttype not in [3, 42]: - self.file.output( - Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont - ) + self.file.output(Op.begin_text, + self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) - self.file.output(self.encode_string(s, fonttype), Op.show, Op.end_text) + self.file.output(self.encode_string(s, fonttype), + Op.show, Op.end_text) # A sequence of characters is broken into multiple chunks. The chunking # serves two purposes: @@ -2641,15 +2426,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # concatenation up front self.file.output(Op.gsave) a = math.radians(angle) - self.file.output( - math.cos(a), - math.sin(a), - -math.sin(a), - math.cos(a), - x, - y, - Op.concat_matrix, - ) + self.file.output(math.cos(a), math.sin(a), + -math.sin(a), math.cos(a), + x, y, Op.concat_matrix) # Emit all the 1-byte characters in a BT/ET group. self.file.output(Op.begin_text) @@ -2661,21 +2440,17 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.file.output( # See pdf spec "Text space details" for the 1000/fontsize # (aka. 1000/T_fs) factor. - [ - ( - -1000 * next(group) / fontsize - if tp == float # a kern - else self.encode_string("".join(group), fonttype) - ) - for tp, group in itertools.groupby(kerns_or_chars, type) - ], - Op.showkern, - ) + [-1000 * next(group) / fontsize if tp == float # a kern + else self.encode_string("".join(group), fonttype) + for tp, group in itertools.groupby(kerns_or_chars, type)], + Op.showkern) prev_start_x = start_x self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. for ft_object, start_x, glyph_idx in multibyte_glyphs: - self._draw_xobject_glyph(ft_object, fontsize, glyph_idx, start_x, 0) + self._draw_xobject_glyph( + ft_object, fontsize, glyph_idx, start_x, 0 + ) self.file.output(Op.grestore) def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): @@ -2684,15 +2459,8 @@ def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, - 0.001 * fontsize, - 0, - 0, - 0.001 * fontsize, - x, - y, - Op.concat_matrix, - Name(name), - Op.use_xobject, + 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, + Name(name), Op.use_xobject, Op.grestore, ) @@ -2712,8 +2480,8 @@ def __init__(self, file): def __repr__(self): d = dict(self.__dict__) - del d["file"] - del d["parent"] + del d['file'] + del d['parent'] return repr(d) def stroke(self): @@ -2724,11 +2492,8 @@ def stroke(self): """ # _linewidth > 0: in pdf a line of width 0 is drawn at minimum # possible device width, but e.g., agg doesn't draw at all - return ( - self._linewidth > 0 - and self._alpha > 0 - and (len(self._rgb) <= 3 or self._rgb[3] != 0.0) - ) + return (self._linewidth > 0 and self._alpha > 0 and + (len(self._rgb) <= 3 or self._rgb[3] != 0.0)) def fill(self, *args): """ @@ -2741,9 +2506,9 @@ def fill(self, *args): _fillcolor = args[0] else: _fillcolor = self._fillcolor - return self._hatch or ( - _fillcolor is not None and (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0) - ) + return (self._hatch or + (_fillcolor is not None and + (len(_fillcolor) <= 3 or _fillcolor[3] != 0.0))) def paint(self): """ @@ -2752,8 +2517,8 @@ def paint(self): """ return Op.paint_path(self.fill(), self.stroke()) - capstyles = {"butt": 0, "round": 1, "projecting": 2} - joinstyles = {"miter": 0, "round": 1, "bevel": 2} + capstyles = {'butt': 0, 'round': 1, 'projecting': 2} + joinstyles = {'miter': 0, 'round': 1, 'bevel': 2} def capstyle_cmd(self, style): return [self.capstyles[style], Op.setlinecap] @@ -2780,19 +2545,15 @@ def hatch_cmd(self, hatch, hatch_color, hatch_linewidth): if self._fillcolor is not None: return self.fillcolor_cmd(self._fillcolor) else: - return [Name("DeviceRGB"), Op.setcolorspace_nonstroke] + return [Name('DeviceRGB'), Op.setcolorspace_nonstroke] else: hatch_style = (hatch_color, self._fillcolor, hatch, hatch_linewidth) name = self.file.hatchPattern(hatch_style) - return [ - Name("Pattern"), - Op.setcolorspace_nonstroke, - name, - Op.setcolor_nonstroke, - ] + return [Name('Pattern'), Op.setcolorspace_nonstroke, + name, Op.setcolor_nonstroke] def rgb_cmd(self, rgb): - if mpl.rcParams["pdf.inheritcolor"]: + if mpl.rcParams['pdf.inheritcolor']: return [] if rgb[0] == rgb[1] == rgb[2]: return [rgb[0], Op.setgray_stroke] @@ -2800,7 +2561,7 @@ def rgb_cmd(self, rgb): return [*rgb[:3], Op.setrgb_stroke] def fillcolor_cmd(self, rgb): - if rgb is None or mpl.rcParams["pdf.inheritcolor"]: + if rgb is None or mpl.rcParams['pdf.inheritcolor']: return [] elif rgb[0] == rgb[1] == rgb[2]: return [rgb[0], Op.setgray_nonstroke] @@ -2824,39 +2585,34 @@ def clip_cmd(self, cliprect, clippath): """Set clip rectangle. Calls `.pop()` and `.push()`.""" cmds = [] # Pop graphics state until we hit the right one or the stack is empty - while (self._cliprect, self._clippath) != ( - cliprect, - clippath, - ) and self.parent is not None: + while ((self._cliprect, self._clippath) != (cliprect, clippath) + and self.parent is not None): cmds.extend(self.pop()) # Unless we hit the right one, set the clip polygon - if (self._cliprect, self._clippath) != ( - cliprect, - clippath, - ) or self.parent is None: + if ((self._cliprect, self._clippath) != (cliprect, clippath) or + self.parent is None): cmds.extend(self.push()) if self._cliprect != cliprect: cmds.extend([cliprect, Op.rectangle, Op.clip, Op.endpath]) if self._clippath != clippath: path, affine = clippath.get_transformed_path_and_affine() cmds.extend( - PdfFile.pathOperations(path, affine, simplify=False) - + [Op.clip, Op.endpath] - ) + PdfFile.pathOperations(path, affine, simplify=False) + + [Op.clip, Op.endpath]) return cmds commands = ( # must come first since may pop - (("_cliprect", "_clippath"), clip_cmd), - (("_alpha", "_forced_alpha", "_effective_alphas"), alpha_cmd), - (("_capstyle",), capstyle_cmd), - (("_fillcolor",), fillcolor_cmd), - (("_joinstyle",), joinstyle_cmd), - (("_linewidth",), linewidth_cmd), - (("_dashes",), dash_cmd), - (("_rgb",), rgb_cmd), + (('_cliprect', '_clippath'), clip_cmd), + (('_alpha', '_forced_alpha', '_effective_alphas'), alpha_cmd), + (('_capstyle',), capstyle_cmd), + (('_fillcolor',), fillcolor_cmd), + (('_joinstyle',), joinstyle_cmd), + (('_linewidth',), linewidth_cmd), + (('_dashes',), dash_cmd), + (('_rgb',), rgb_cmd), # must come after fillcolor and rgb - (("_hatch", "_hatch_color", "_hatch_linewidth"), hatch_cmd), + (('_hatch', '_hatch_color', '_hatch_linewidth'), hatch_cmd), ) def delta(self, other): @@ -2882,16 +2638,17 @@ def delta(self, other): # This should be removed when numpy < 1.25 is no longer supported. ours = np.asarray(ours) theirs = np.asarray(theirs) - different = ours.shape != theirs.shape or np.any(ours != theirs) + different = (ours.shape != theirs.shape or + np.any(ours != theirs)) if different: break # Need to update hatching if we also updated fillcolor - if cmd.__name__ == "hatch_cmd" and fill_performed: + if cmd.__name__ == 'hatch_cmd' and fill_performed: different = True if different: - if cmd.__name__ == "fillcolor_cmd": + if cmd.__name__ == 'fillcolor_cmd': fill_performed = True theirs = [getattr(other, p) for p in params] cmds.extend(cmd(self, *theirs)) @@ -2904,8 +2661,9 @@ def copy_properties(self, other): Copy properties of other into self. """ super().copy_properties(other) - fillcolor = getattr(other, "_fillcolor", self._fillcolor) - effective_alphas = getattr(other, "_effective_alphas", self._effective_alphas) + fillcolor = getattr(other, '_fillcolor', self._fillcolor) + effective_alphas = getattr(other, '_effective_alphas', + self._effective_alphas) self._fillcolor = fillcolor self._effective_alphas = effective_alphas @@ -2940,9 +2698,8 @@ class PdfPages: confusion when using `~.pyplot.savefig` and forgetting the format argument. """ - @_api.delete_parameter( - "3.10", "keep_empty", addendum="This parameter does nothing." - ) + @_api.delete_parameter("3.10", "keep_empty", + addendum="This parameter does nothing.") def __init__(self, filename, keep_empty=None, metadata=None): """ Create a new PdfPages object. @@ -3037,12 +2794,13 @@ class FigureCanvasPdf(FigureCanvasBase): # docstring inherited fixed_dpi = 72 - filetypes = {"pdf": "Portable Document Format"} + filetypes = {'pdf': 'Portable Document Format'} def get_default_filetype(self): - return "pdf" + return 'pdf' - def print_pdf(self, filename, *, bbox_inches_restore=None, metadata=None): + def print_pdf(self, filename, *, + bbox_inches_restore=None, metadata=None): dpi = self.figure.dpi self.figure.dpi = 72 # there are 72 pdf points to an inch @@ -3054,13 +2812,9 @@ def print_pdf(self, filename, *, bbox_inches_restore=None, metadata=None): try: file.newPage(width, height) renderer = MixedModeRenderer( - self.figure, - width, - height, - dpi, + self.figure, width, height, dpi, RendererPdf(file, dpi, height, width), - bbox_inches_restore=bbox_inches_restore, - ) + bbox_inches_restore=bbox_inches_restore) self.figure.draw(renderer) renderer.finalize() if not isinstance(filename, PdfPages): @@ -3068,7 +2822,7 @@ def print_pdf(self, filename, *, bbox_inches_restore=None, metadata=None): finally: if isinstance(filename, PdfPages): # finish off this page file.endStream() - else: # we opened the file above; now finish it off + else: # we opened the file above; now finish it off file.close() def draw(self): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 6edeedcb0f46..0cb6430ec823 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -16,11 +16,7 @@ import matplotlib as mpl from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( - _Backend, - FigureCanvasBase, - FigureManagerBase, - RendererBase, -) + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC @@ -77,12 +73,12 @@ def _escape_cdata(s): return s -_escape_xml_comment = re.compile(r"-(?=-)") +_escape_xml_comment = re.compile(r'-(?=-)') def _escape_comment(s): s = _escape_cdata(s) - return _escape_xml_comment.sub("- ", s) + return _escape_xml_comment.sub('- ', s) def _escape_attrib(s): @@ -95,15 +91,9 @@ def _escape_attrib(s): def _quote_escape_attrib(s): - return ( - '"' + _escape_cdata(s) + '"' - if '"' not in s - else ( - "'" + _escape_cdata(s) + "'" - if "'" not in s - else '"' + _escape_attrib(s) + '"' - ) - ) + return ('"' + _escape_cdata(s) + '"' if '"' not in s else + "'" + _escape_cdata(s) + "'" if "'" not in s else + '"' + _escape_attrib(s) + '"') def _short_float_fmt(x): @@ -111,7 +101,7 @@ 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 f"{x:f}".rstrip("0").rstrip(".") + return f'{x:f}'.rstrip('0').rstrip('.') class XMLWriter: @@ -139,7 +129,7 @@ def __flush(self, indent=True): self.__write(">") self.__open = 0 if self.__data: - data = "".join(self.__data) + data = ''.join(self.__data) self.__write(_escape_cdata(data)) self.__data = [] @@ -166,13 +156,13 @@ def start(self, tag, attrib={}, **extra): tag = _escape_cdata(tag) self.__data = [] self.__tags.append(tag) - self.__write(self.__indentation[: len(self.__tags) - 1]) + self.__write(self.__indentation[:len(self.__tags) - 1]) self.__write(f"<{tag}") for k, v in {**attrib, **extra}.items(): if v: k = _escape_cdata(k) v = _quote_escape_attrib(v) - self.__write(f" {k}={v}") + self.__write(f' {k}={v}') self.__open = 1 return len(self.__tags) - 1 @@ -186,7 +176,7 @@ def comment(self, comment): Comment text. """ self.__flush() - self.__write(self.__indentation[: len(self.__tags)]) + self.__write(self.__indentation[:len(self.__tags)]) self.__write(f"\n") def data(self, text): @@ -214,9 +204,8 @@ def end(self, tag=None, indent=True): """ if tag: assert self.__tags, f"unbalanced end({tag})" - assert ( - _escape_cdata(tag) == self.__tags[-1] - ), f"expected end({self.__tags[-1]}), got {tag}" + assert _escape_cdata(tag) == self.__tags[-1], \ + f"expected end({self.__tags[-1]}), got {tag}" else: assert self.__tags, "unbalanced end()" tag = self.__tags.pop() @@ -227,7 +216,7 @@ def end(self, tag=None, indent=True): self.__write("/>\n") return if indent: - self.__write(self.__indentation[: len(self.__tags)]) + self.__write(self.__indentation[:len(self.__tags)]) self.__write(f"\n") def close(self, id): @@ -262,56 +251,44 @@ def flush(self): def _generate_transform(transform_list): parts = [] 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,) - ): + 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): + if type == 'matrix' and isinstance(value, Affine2DBase): value = value.to_values() - parts.append( - "{}({})".format(type, " ".join(_short_float_fmt(x) for x in value)) - ) - return " ".join(parts) + parts.append('{}({})'.format( + type, ' '.join(_short_float_fmt(x) for x in value))) + return ' '.join(parts) def _generate_css(attrib): return "; ".join(f"{k}: {v}" for k, v in attrib.items()) -_capstyle_d = {"projecting": "square", "butt": "butt", "round": "round"} +_capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'} def _check_is_str(info, key): if not isinstance(info, str): - raise TypeError( - f"Invalid type for {key} metadata. Expected str, not " f"{type(info)}." - ) + raise TypeError(f'Invalid type for {key} metadata. Expected str, not ' + f'{type(info)}.') def _check_is_iterable_of_str(infos, key): if np.iterable(infos): for info in infos: if not isinstance(info, str): - raise TypeError( - f"Invalid type for {key} metadata. Expected " - f"iterable of str, not {type(info)}." - ) + raise TypeError(f'Invalid type for {key} metadata. Expected ' + f'iterable of str, not {type(info)}.') else: - raise TypeError( - f"Invalid type for {key} metadata. Expected str or " - f"iterable of str, not {type(infos)}." - ) + raise TypeError(f'Invalid type for {key} metadata. Expected str or ' + f'iterable of str, not {type(infos)}.') class RendererSVG(RendererBase): - def __init__( - self, width, height, svgwriter, basename=None, image_dpi=72, *, metadata=None - ): + def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, + *, metadata=None): self.width = width self.height = height self.writer = XMLWriter(svgwriter) @@ -339,15 +316,14 @@ def __init__( str_width = _short_float_fmt(width) svgwriter.write(svgProlog) self._start_id = self.writer.start( - "svg", - width=f"{str_width}pt", - height=f"{str_height}pt", - viewBox=f"0 0 {str_width} {str_height}", + 'svg', + width=f'{str_width}pt', + height=f'{str_height}pt', + viewBox=f'0 0 {str_width} {str_height}', xmlns="http://www.w3.org/2000/svg", version="1.1", - id=mpl.rcParams["svg.id"], - attrib={"xmlns:xlink": "http://www.w3.org/1999/xlink"}, - ) + id=mpl.rcParams['svg.id'], + attrib={'xmlns:xlink': "http://www.w3.org/1999/xlink"}) self._write_metadata(metadata) self._write_default_style() @@ -378,20 +354,21 @@ def _write_metadata(self, metadata): if metadata is None: metadata = {} metadata = { - "Format": "image/svg+xml", - "Type": "http://purl.org/dc/dcmitype/StillImage", - "Creator": f"Matplotlib v{mpl.__version__}, https://matplotlib.org/", - **metadata, + 'Format': 'image/svg+xml', + 'Type': 'http://purl.org/dc/dcmitype/StillImage', + 'Creator': + f'Matplotlib v{mpl.__version__}, https://matplotlib.org/', + **metadata } writer = self.writer - if "Title" in metadata: - title = metadata["Title"] - _check_is_str(title, "Title") - writer.element("title", text=title) + if 'Title' in metadata: + title = metadata['Title'] + _check_is_str(title, 'Title') + writer.element('title', text=title) # Special handling. - date = metadata.get("Date", None) + date = metadata.get('Date', None) if date is not None: if isinstance(date, str): dates = [date] @@ -406,70 +383,54 @@ def _write_metadata(self, metadata): dates.append(d.isoformat()) else: raise TypeError( - f"Invalid type for Date metadata. " - f"Expected iterable of str, date, or datetime, " - f"not {type(d)}." - ) + f'Invalid type for Date metadata. ' + f'Expected iterable of str, date, or datetime, ' + f'not {type(d)}.') else: - raise TypeError( - f"Invalid type for Date metadata. " - f"Expected str, date, datetime, or iterable " - f"of the same, not {type(date)}." - ) - metadata["Date"] = "/".join(dates) - elif "Date" not in metadata: + raise TypeError(f'Invalid type for Date metadata. ' + f'Expected str, date, datetime, or iterable ' + f'of the same, not {type(date)}.') + metadata['Date'] = '/'.join(dates) + elif 'Date' not in metadata: # Do not add `Date` if the user explicitly set `Date` to `None` # Get source date from SOURCE_DATE_EPOCH, if set. # See https://reproducible-builds.org/specs/source-date-epoch/ date = os.getenv("SOURCE_DATE_EPOCH") if date: date = datetime.datetime.fromtimestamp(int(date), datetime.timezone.utc) - metadata["Date"] = date.replace(tzinfo=UTC).isoformat() + metadata['Date'] = date.replace(tzinfo=UTC).isoformat() else: - metadata["Date"] = datetime.datetime.today().isoformat() + metadata['Date'] = datetime.datetime.today().isoformat() mid = None - def ensure_metadata(mid): if mid is not None: return mid - mid = writer.start("metadata") - writer.start( - "rdf:RDF", - attrib={ - "xmlns:dc": "http://purl.org/dc/elements/1.1/", - "xmlns:cc": "http://creativecommons.org/ns#", - "xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - }, - ) - writer.start("cc:Work") + mid = writer.start('metadata') + writer.start('rdf:RDF', attrib={ + 'xmlns:dc': "http://purl.org/dc/elements/1.1/", + 'xmlns:cc': "http://creativecommons.org/ns#", + 'xmlns:rdf': "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + }) + writer.start('cc:Work') return mid - uri = metadata.pop("Type", None) + uri = metadata.pop('Type', None) if uri is not None: mid = ensure_metadata(mid) - writer.element("dc:type", attrib={"rdf:resource": uri}) + writer.element('dc:type', attrib={'rdf:resource': uri}) # Single value only. - for key in [ - "Title", - "Coverage", - "Date", - "Description", - "Format", - "Identifier", - "Language", - "Relation", - "Source", - ]: + for key in ['Title', 'Coverage', 'Date', 'Description', 'Format', + 'Identifier', 'Language', 'Relation', 'Source']: info = metadata.pop(key, None) if info is not None: mid = ensure_metadata(mid) _check_is_str(info, key) - writer.element(f"dc:{key.lower()}", text=info) + writer.element(f'dc:{key.lower()}', text=info) # Multiple Agent values. - for key in ["Creator", "Contributor", "Publisher", "Rights"]: + for key in ['Creator', 'Contributor', 'Publisher', 'Rights']: agents = metadata.pop(key, None) if agents is None: continue @@ -480,53 +441,52 @@ def ensure_metadata(mid): _check_is_iterable_of_str(agents, key) # Now we know that we have an iterable of str mid = ensure_metadata(mid) - writer.start(f"dc:{key.lower()}") + writer.start(f'dc:{key.lower()}') for agent in agents: - writer.start("cc:Agent") - writer.element("dc:title", text=agent) - writer.end("cc:Agent") - writer.end(f"dc:{key.lower()}") + writer.start('cc:Agent') + writer.element('dc:title', text=agent) + writer.end('cc:Agent') + writer.end(f'dc:{key.lower()}') # Multiple values. - keywords = metadata.pop("Keywords", None) + keywords = metadata.pop('Keywords', None) if keywords is not None: if isinstance(keywords, str): keywords = [keywords] - _check_is_iterable_of_str(keywords, "Keywords") + _check_is_iterable_of_str(keywords, 'Keywords') # Now we know that we have an iterable of str mid = ensure_metadata(mid) - writer.start("dc:subject") - writer.start("rdf:Bag") + writer.start('dc:subject') + writer.start('rdf:Bag') for keyword in keywords: - writer.element("rdf:li", text=keyword) - writer.end("rdf:Bag") - writer.end("dc:subject") + writer.element('rdf:li', text=keyword) + writer.end('rdf:Bag') + writer.end('dc:subject') if mid is not None: writer.close(mid) if metadata: - raise ValueError( - "Unknown metadata key(s) passed to SVG writer: " + ",".join(metadata) - ) + raise ValueError('Unknown metadata key(s) passed to SVG writer: ' + + ','.join(metadata)) def _write_default_style(self): writer = self.writer - default_style = _generate_css( - {"stroke-linejoin": "round", "stroke-linecap": "butt"} - ) - writer.start("defs") - writer.element("style", type="text/css", text="*{%s}" % default_style) - writer.end("defs") + default_style = _generate_css({ + 'stroke-linejoin': 'round', + 'stroke-linecap': 'butt'}) + writer.start('defs') + writer.element('style', type='text/css', text='*{%s}' % default_style) + writer.end('defs') def _make_id(self, type, content): - salt = mpl.rcParams["svg.hashsalt"] + salt = mpl.rcParams['svg.hashsalt'] if salt is None: salt = str(uuid.uuid4()) m = hashlib.sha256() - m.update(salt.encode("utf8")) - m.update(str(content).encode("utf8")) - return f"{type}{m.hexdigest()[:10]}" + m.update(salt.encode('utf8')) + m.update(str(content).encode('utf8')) + return f'{type}{m.hexdigest()[:10]}' def _make_flip_transform(self, transform): return transform + Affine2D().scale(1, -1).translate(0, self.height) @@ -544,7 +504,7 @@ def _get_hatch(self, gc, rgbFace): dictkey = (gc.get_hatch(), rgbFace, edge, lw) oid = self._hatchd.get(dictkey) if oid is None: - oid = self._make_id("h", dictkey) + oid = self._make_id('h', dictkey) self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge, lw), oid) else: _, oid = oid @@ -555,46 +515,44 @@ def _write_hatches(self): return HATCH_SIZE = 72 writer = self.writer - writer.start("defs") + writer.start('defs') for (path, face, stroke, lw), oid in self._hatchd.values(): writer.start( - "pattern", + 'pattern', id=oid, patternUnits="userSpaceOnUse", - x="0", - y="0", - width=str(HATCH_SIZE), - height=str(HATCH_SIZE), - ) + 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, - ) + Affine2D() + .scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE), + simplify=False) if face is None: - fill = "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, - ) + 'rect', + x="0", y="0", width=str(HATCH_SIZE+1), + height=str(HATCH_SIZE+1), + fill=fill) hatch_style = { - "fill": rgb2hex(stroke), - "stroke": rgb2hex(stroke), - "stroke-width": str(lw), - "stroke-linecap": "butt", - "stroke-linejoin": "miter", - } + 'fill': rgb2hex(stroke), + 'stroke': rgb2hex(stroke), + 'stroke-width': str(lw), + 'stroke-linecap': 'butt', + 'stroke-linejoin': 'miter' + } if stroke[3] < 1: - hatch_style["stroke-opacity"] = str(stroke[3]) - writer.element("path", d=path_data, style=_generate_css(hatch_style)) - writer.end("pattern") - writer.end("defs") + hatch_style['stroke-opacity'] = str(stroke[3]) + writer.element( + 'path', + d=path_data, + style=_generate_css(hatch_style) + ) + writer.end('pattern') + writer.end('defs') def _get_style_dict(self, gc, rgbFace): """Generate a style string from the GraphicsContext and rgbFace.""" @@ -603,43 +561,41 @@ def _get_style_dict(self, gc, rgbFace): forced_alpha = gc.get_forced_alpha() if gc.get_hatch() is not None: - attrib["fill"] = f"url(#{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]) + attrib['fill'] = f"url(#{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" + 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]) + 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()) + 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)) + 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) + attrib['stroke'] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: - attrib["stroke-opacity"] = _short_float_fmt(rgb[3]) + 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()] + 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 @@ -654,103 +610,88 @@ def _get_clip_attrs(self, gc): dictkey = (self._get_clippath_id(clippath), str(clippath_trans)) elif cliprect is not None: x, y, w, h = cliprect.bounds - y = self.height - (y + h) + y = self.height-(y+h) dictkey = (x, y, w, h) else: return {} clip = self._clipd.get(dictkey) if clip is None: - oid = self._make_id("p", dictkey) + 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: _, oid = clip - return {"clip-path": f"url(#{oid})"} + return {'clip-path': f'url(#{oid})'} def _write_clips(self): if not len(self._clipd): return writer = self.writer - writer.start("defs") + writer.start('defs') for clip, oid in self._clipd.values(): - writer.start("clipPath", id=oid) + 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) + 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", + '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") + 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) + self.writer.start('g', id=gid) else: self._groupd[s] = self._groupd.get(s, 0) + 1 - self.writer.start("g", id=f"{s}_{self._groupd[s]:d}") + self.writer.start('g', id=f"{s}_{self._groupd[s]:d}") def close_group(self, s): # docstring inherited - self.writer.end("g") + self.writer.end('g') def option_image_nocomposite(self): # docstring inherited - return not mpl.rcParams["image.composite_image"] + return not mpl.rcParams['image.composite_image'] - def _convert_path( - self, path, transform=None, clip=None, simplify=None, sketch=None - ): + 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") + 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 + 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(), - ) + path, trans_and_flip, clip=clip, simplify=simplify, + sketch=gc.get_sketch_params()) if gc.get_url() is not None: - self.writer.start("a", {"xlink:href": gc.get_url()}) - self.writer.element( - "path", - d=path_data, - **self._get_clip_attrs(gc), - style=self._get_style(gc, rgbFace), - ) + self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), + style=self._get_style(gc, rgbFace)) if gc.get_url() is not None: - self.writer.end("a") + self.writer.end('a') - def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): + def draw_markers( + self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited if not len(path.vertices): @@ -758,59 +699,44 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) writer = self.writer path_data = self._convert_path( - marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False - ) + 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")} - ) + 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") + 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 - writer.start("g", **self._get_clip_attrs(gc)) + writer.start('g', **self._get_clip_attrs(gc)) if gc.get_url() is not None: - self.writer.start("a", {"xlink:href": gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url()}) trans_and_flip = self._make_flip_transform(trans) - attrib = {"xlink:href": f"#{oid}"} - clip = (0, 0, self.width * 72, self.height * 72) + attrib = {'xlink:href': f'#{oid}'} + clip = (0, 0, self.width*72, self.height*72) for vertices, code in path.iter_segments( - trans_and_flip, clip=clip, simplify=False - ): + 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) + attrib['x'] = _short_float_fmt(x) + attrib['y'] = _short_float_fmt(y) + attrib['style'] = self._get_style(gc, rgbFace) + writer.element('use', attrib=attrib) if gc.get_url() is not None: - self.writer.end("a") - writer.end("g") - - def draw_path_collection( - self, - gc, - master_transform, - paths, - all_transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - *, - hatchcolors=None, - ): + self.writer.end('a') + writer.end('g') + + def draw_path_collection(self, gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position, *, hatchcolors=None): if hatchcolors is None: hatchcolors = [] # Is the optimization worth it? Rough calculation: @@ -820,75 +746,50 @@ def draw_path_collection( # (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 = ( + 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 super().draw_path_collection( - gc, - master_transform, - paths, - all_transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - hatchcolors=hatchcolors, - ) + gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position, hatchcolors=hatchcolors) writer = self.writer path_codes = [] - writer.start("defs") - for i, (path, transform) in enumerate( - self._iter_collection_raw_paths(master_transform, paths, all_transforms) - ): + 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=path.should_simplify) - oid = "C{:x}_{:x}_{}".format( - self._path_collection_id, i, self._make_id("", d) - ) - writer.element("path", id=oid, d=d) + d = self._convert_path(path, transform, simplify=False) + oid = 'C{:x}_{:x}_{}'.format( + self._path_collection_id, i, self._make_id('', d)) + writer.element('path', id=oid, d=d) path_codes.append(oid) - writer.end("defs") + writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, - path_codes, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - linestyles, - antialiaseds, - urls, - offset_position, - hatchcolors=hatchcolors, - ): + gc, path_codes, offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position, hatchcolors=hatchcolors): url = gc0.get_url() if url is not None: - writer.start("a", attrib={"xlink:href": url}) + writer.start('a', attrib={'xlink:href': url}) clip_attrs = self._get_clip_attrs(gc0) if clip_attrs: - writer.start("g", **clip_attrs) + writer.start('g', **clip_attrs) attrib = { - "xlink:href": f"#{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) + 'xlink:href': f'#{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 clip_attrs: - writer.end("g") + writer.end('g') if url is not None: - writer.end("a") + writer.end('a') self._path_collection_id += 1 @@ -910,7 +811,7 @@ def _draw_gouraud_triangle(self, transformed_points, colors): return writer = self.writer - writer.start("defs") + writer.start('defs') for i in range(3): x1, y1 = transformed_points[i] x2, y2 = transformed_points[(i + 1) % 3] @@ -932,126 +833,102 @@ def _draw_gouraud_triangle(self, transformed_points, colors): yb = m2 * xb + b2 writer.start( - "linearGradient", + 'linearGradient', id=f"GR{self._n_gradients:x}_{i:d}", gradientUnits="userSpaceOnUse", - x1=_short_float_fmt(x1), - y1=_short_float_fmt(y1), - x2=_short_float_fmt(xb), - y2=_short_float_fmt(yb), - ) + x1=_short_float_fmt(x1), y1=_short_float_fmt(y1), + x2=_short_float_fmt(xb), y2=_short_float_fmt(yb)) writer.element( - "stop", - offset="1", - style=_generate_css( - { - "stop-color": rgb2hex(avg_color), - "stop-opacity": _short_float_fmt(rgba_color[-1]), - } - ), - ) + 'stop', + offset='1', + style=_generate_css({ + 'stop-color': rgb2hex(avg_color), + 'stop-opacity': _short_float_fmt(rgba_color[-1])})) writer.element( - "stop", - offset="0", - style=_generate_css( - {"stop-color": rgb2hex(rgba_color), "stop-opacity": "0"} - ), - ) + 'stop', + offset='0', + style=_generate_css({'stop-color': rgb2hex(rgba_color), + 'stop-opacity': "0"})) - writer.end("linearGradient") + writer.end('linearGradient') - writer.end("defs") + writer.end('defs') # triangle formation using "path" - dpath = ( - f"M {_short_float_fmt(x1)},{_short_float_fmt(y1)}" - f" L {_short_float_fmt(x2)},{_short_float_fmt(y2)}" - f" {_short_float_fmt(x3)},{_short_float_fmt(y3)} Z" - ) + dpath = (f"M {_short_float_fmt(x1)},{_short_float_fmt(y1)}" + f" L {_short_float_fmt(x2)},{_short_float_fmt(y2)}" + f" {_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", - }, - ) + '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)", - }, - ) + 'g', + attrib={'stroke': "none", + 'stroke-width': "0", + 'shape-rendering': "crispEdges", + 'filter': "url(#colorMat)"}) writer.element( - "path", - attrib={ - "d": dpath, - "fill": f"url(#GR{self._n_gradients:x}_0)", - "shape-rendering": "crispEdges", - }, - ) + 'path', + attrib={'d': dpath, + 'fill': f'url(#GR{self._n_gradients:x}_0)', + 'shape-rendering': "crispEdges"}) writer.element( - "path", - attrib={ - "d": dpath, - "fill": f"url(#GR{self._n_gradients:x}_1)", - "filter": "url(#colorAdd)", - "shape-rendering": "crispEdges", - }, - ) + 'path', + attrib={'d': dpath, + 'fill': f'url(#GR{self._n_gradients:x}_1)', + 'filter': 'url(#colorAdd)', + 'shape-rendering': "crispEdges"}) writer.element( - "path", - attrib={ - "d": dpath, - "fill": f"url(#GR{self._n_gradients:x}_2)", - "filter": "url(#colorAdd)", - "shape-rendering": "crispEdges", - }, - ) + 'path', + attrib={'d': dpath, + 'fill': f'url(#GR{self._n_gradients:x}_2)', + 'filter': 'url(#colorAdd)', + 'shape-rendering': "crispEdges"}) - writer.end("g") + writer.end('g') self._n_gradients += 1 - def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): + def draw_gouraud_triangles(self, gc, triangles_array, colors_array, + transform): writer = self.writer - writer.start("g", **self._get_clip_attrs(gc)) + writer.start('g', **self._get_clip_attrs(gc)) transform = transform.frozen() trans_and_flip = self._make_flip_transform(transform) if not self._has_gouraud: self._has_gouraud = True - writer.start("filter", id="colorAdd") + writer.start( + 'filter', + id='colorAdd') writer.element( - "feComposite", - attrib={"in": "SourceGraphic"}, - in2="BackgroundImage", - operator="arithmetic", - k2="1", - k3="1", - ) - writer.end("filter") + '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.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") + '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') for points, colors in zip(triangles_array, colors_array): self._draw_gouraud_triangle(trans_and_flip.transform(points), colors) - writer.end("g") + writer.end('g') def option_scale_image(self): # docstring inherited @@ -1072,76 +949,71 @@ def draw_image(self, gc, x, y, im, transform=None): if clip_attrs: # 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", **clip_attrs) + self.writer.start('g', **clip_attrs) url = gc.get_url() if url is not None: - self.writer.start("a", attrib={"xlink:href": url}) + self.writer.start('a', attrib={'xlink:href': url}) attrib = {} oid = gc.get_gid() - if mpl.rcParams["svg.image_inline"]: + if mpl.rcParams['svg.image_inline']: buf = BytesIO() Image.fromarray(im).save(buf, format="png") - oid = oid or self._make_id("image", buf.getvalue()) - attrib["xlink:href"] = "data:image/png;base64,\n" + base64.b64encode( - buf.getvalue() - ).decode("ascii") + oid = oid or self._make_id('image', buf.getvalue()) + attrib['xlink:href'] = ( + "data:image/png;base64,\n" + + base64.b64encode(buf.getvalue()).decode('ascii')) else: if self.basename is None: - raise ValueError( - "Cannot save image data to filesystem when " - "writing SVG to an in-memory buffer" - ) - filename = f"{self.basename}.image{next(self._image_counter)}.png" - _log.info("Writing image file for inclusion: %s", filename) + raise ValueError("Cannot save image data to filesystem when " + "writing SVG to an in-memory buffer") + filename = f'{self.basename}.image{next(self._image_counter)}.png' + _log.info('Writing image file for inclusion: %s', filename) Image.fromarray(im).save(filename) - oid = oid or "Im_" + self._make_id("image", filename) - attrib["xlink:href"] = filename - attrib["id"] = oid + 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))] - ), + '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, - ) + 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) + attrib['opacity'] = _short_float_fmt(alpha) flipped = ( - Affine2D().scale(1.0 / w, 1.0 / h) - + transform - + Affine2D() + Affine2D().scale(1.0 / w, 1.0 / h) + + transform + + Affine2D() .translate(x, y) .scale(1.0, -1.0) - .translate(0.0, self.height) - ) + .translate(0.0, self.height)) - attrib["transform"] = _generate_transform([("matrix", flipped.frozen())]) - attrib["style"] = "image-rendering:crisp-edges;" "image-rendering:pixelated" + attrib['transform'] = _generate_transform( + [('matrix', flipped.frozen())]) + attrib['style'] = ( + 'image-rendering:crisp-edges;' + 'image-rendering:pixelated') self.writer.element( - "image", - width=_short_float_fmt(w), - height=_short_float_fmt(h), - attrib=attrib, - ) + 'image', + width=_short_float_fmt(w), height=_short_float_fmt(h), + attrib=attrib) if url is not None: - self.writer.end("a") + self.writer.end('a') if clip_attrs: - self.writer.end("g") + self.writer.end('g') def _update_glyph_map_defs(self, glyph_map_new): """ @@ -1150,20 +1022,16 @@ def _update_glyph_map_defs(self, glyph_map_new): """ writer = self.writer if glyph_map_new: - writer.start("defs") + writer.start('defs') for char_id, (vertices, codes) in glyph_map_new.items(): char_id = self._adjust_char_id(char_id) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( - Path(vertices * 64, codes), simplify=False - ) + Path(vertices * 64, codes), simplify=False) writer.element( - "path", - id=char_id, - d=path_data, - transform=_generate_transform([("scale", (1 / 64,))]), - ) - writer.end("defs") + 'path', id=char_id, d=path_data, + transform=_generate_transform([('scale', (1 / 64,))])) + writer.end('defs') self._glyph_map.update(glyph_map_new) def _adjust_char_id(self, char_id): @@ -1182,75 +1050,63 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): fontsize = prop.get_size_in_points() style = {} - if color != "#000000": - style["fill"] = color + 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) + style['opacity'] = _short_float_fmt(alpha) font_scale = fontsize / text2path.FONT_SCALE attrib = { - "style": _generate_css(style), - "transform": _generate_transform( - [ - ("translate", (x, y)), - ("rotate", (-angle,)), - ("scale", (font_scale, -font_scale)), - ] - ), + 'style': _generate_css(style), + 'transform': _generate_transform([ + ('translate', (x, y)), + ('rotate', (-angle,)), + ('scale', (font_scale, -font_scale))]), } - writer.start("g", attrib=attrib) + writer.start('g', attrib=attrib) if not ismath: font = text2path._get_font(prop) _glyphs = text2path.get_glyphs_with_font( - font, s, glyph_map=glyph_map, return_new_glyphs_only=True - ) + font, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for glyph_id, xposition, yposition, scale in glyph_info: writer.element( - "use", - transform=_generate_transform( - [ - ("translate", (xposition, yposition)), - ("scale", (scale,)), - ] - ), - attrib={"xlink:href": f"#{glyph_id}"}, - ) + 'use', + transform=_generate_transform([ + ('translate', (xposition, yposition)), + ('scale', (scale,)), + ]), + attrib={'xlink:href': f'#{glyph_id}'}) else: if ismath == "TeX": _glyphs = text2path.get_glyphs_tex( - prop, s, glyph_map=glyph_map, return_new_glyphs_only=True - ) + 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 - ) + prop, s, glyph_map=glyph_map, return_new_glyphs_only=True) glyph_info, glyph_map_new, rects = _glyphs self._update_glyph_map_defs(glyph_map_new) for char_id, xposition, yposition, scale in glyph_info: char_id = self._adjust_char_id(char_id) writer.element( - "use", - transform=_generate_transform( - [ - ("translate", (xposition, yposition)), - ("scale", (scale,)), - ] - ), - attrib={"xlink:href": f"#{char_id}"}, - ) + 'use', + transform=_generate_transform([ + ('translate', (xposition, yposition)), + ('scale', (scale,)), + ]), + attrib={'xlink:href': f'#{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.element('path', d=path_data) - writer.end("g") + writer.end('g') def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): # NOTE: If you change the font styling CSS, then be sure the check for @@ -1263,27 +1119,27 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): color = rgb2hex(gc.get_rgb()) font_style = {} color_style = {} - if color != "#000000": - color_style["fill"] = color + if color != '#000000': + color_style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - color_style["opacity"] = _short_float_fmt(alpha) + color_style['opacity'] = _short_float_fmt(alpha) if not ismath: attrib = {} # Separate font style in their separate attributes - if prop.get_style() != "normal": - font_style["font-style"] = prop.get_style() - if prop.get_variant() != "normal": - font_style["font-variant"] = prop.get_variant() + if prop.get_style() != 'normal': + font_style['font-style'] = prop.get_style() + if prop.get_variant() != 'normal': + font_style['font-variant'] = prop.get_variant() weight = fm.weight_dict[prop.get_weight()] if weight != 400: - font_style["font-weight"] = f"{weight}" + font_style['font-weight'] = f'{weight}' def _normalize_sans(name): - return "sans-serif" if name in ["sans", "sans serif"] else name + return 'sans-serif' if name in ['sans', 'sans serif'] else name def _expand_family_entry(fn): fn = _normalize_sans(fn) @@ -1299,21 +1155,19 @@ def _expand_family_entry(fn): def _get_all_quoted_names(prop): # only quote specific names, not generic names - return [ - name if name in fm.font_family_aliases else repr(name) - for entry in prop.get_family() - for name in _expand_family_entry(entry) - ] + return [name if name in fm.font_family_aliases else repr(name) + for entry in prop.get_family() + for name in _expand_family_entry(entry)] - font_style["font-size"] = f"{_short_float_fmt(prop.get_size())}px" + font_style['font-size'] = f'{_short_float_fmt(prop.get_size())}px' # ensure expansion, quoting, and dedupe of font names - font_style["font-family"] = ", ".join( + font_style['font-family'] = ", ".join( dict.fromkeys(_get_all_quoted_names(prop)) - ) + ) - if prop.get_stretch() != "normal": - font_style["font-stretch"] = prop.get_stretch() - attrib["style"] = _generate_css({**font_style, **color_style}) + if prop.get_stretch() != 'normal': + font_style['font-stretch'] = prop.get_stretch() + attrib['style'] = _generate_css({**font_style, **color_style}) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original @@ -1333,41 +1187,39 @@ def _get_all_quoted_names(prop): ax = ax + v_offset * dir_vert[0] ay = ay + v_offset * dir_vert[1] - ha_mpl_to_svg = {"left": "start", "right": "end", "center": "middle"} - font_style["text-anchor"] = ha_mpl_to_svg[mtext.get_ha()] + ha_mpl_to_svg = {'left': 'start', 'right': 'end', + 'center': 'middle'} + font_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({**font_style, **color_style}) - attrib["transform"] = _generate_transform( - [("rotate", (-angle, ax, ay))] - ) + attrib['x'] = _short_float_fmt(ax) + attrib['y'] = _short_float_fmt(ay) + attrib['style'] = _generate_css({**font_style, **color_style}) + attrib['transform'] = _generate_transform([ + ("rotate", (-angle, ax, ay))]) else: - attrib["transform"] = _generate_transform( - [("translate", (x, y)), ("rotate", (-angle,))] - ) + attrib['transform'] = _generate_transform([ + ('translate', (x, y)), + ('rotate', (-angle,))]) - writer.element("text", s, attrib=attrib) + writer.element('text', s, attrib=attrib) else: writer.comment(s) - width, height, descent, glyphs, rects = ( + width, height, descent, glyphs, rects = \ self._text2path.mathtext_parser.parse(s, 72, prop) - ) # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. - writer.start( - "g", - style=_generate_css({**font_style, **color_style}), - transform=_generate_transform( - [("translate", (x, y)), ("rotate", (-angle,))] - ), - ) + writer.start('g', + style=_generate_css({**font_style, **color_style}), + transform=_generate_transform([ + ('translate', (x, y)), + ('rotate', (-angle,))]), + ) - writer.start("text") + writer.start('text') # Sort the characters by font, and output one tspan for each. spans = {} @@ -1375,44 +1227,43 @@ def _get_all_quoted_names(prop): entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes - if entry.style != "normal": - font_style["font-style"] = entry.style - if entry.variant != "normal": - font_style["font-variant"] = entry.variant + if entry.style != 'normal': + font_style['font-style'] = entry.style + if entry.variant != 'normal': + font_style['font-variant'] = entry.variant if entry.weight != 400: - font_style["font-weight"] = f"{entry.weight}" - font_style["font-size"] = f"{_short_float_fmt(fontsize)}px" - font_style["font-family"] = f"{entry.name!r}" # ensure quoting - if entry.stretch != "normal": - font_style["font-stretch"] = entry.stretch + font_style['font-weight'] = f'{entry.weight}' + font_style['font-size'] = f'{_short_float_fmt(fontsize)}px' + font_style['font-family'] = f'{entry.name!r}' # ensure quoting + if entry.stretch != 'normal': + font_style['font-stretch'] = entry.stretch style = _generate_css({**font_style, **color_style}) if thetext == 32: - thetext = 0xA0 # non-breaking space + thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position for x, y, t in chars: # Output one tspan for each character writer.element( - "tspan", + 'tspan', chr(t), x=_short_float_fmt(x), y=_short_float_fmt(y), - style=style, - ) + style=style) - writer.end("text") + writer.end('text') for x, y, width, height in rects: writer.element( - "rect", + 'rect', x=_short_float_fmt(x), - y=_short_float_fmt(-y - 1), + y=_short_float_fmt(-y-1), width=_short_float_fmt(width), - height=_short_float_fmt(height), - ) + height=_short_float_fmt(height) + ) - writer.end("g") + writer.end('g') def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited @@ -1421,21 +1272,21 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if clip_attrs: # Cannot apply clip-path directly to the text, because # it has a transformation - self.writer.start("g", **clip_attrs) + self.writer.start('g', **clip_attrs) if gc.get_url() is not None: - self.writer.start("a", {"xlink:href": gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url()}) - if mpl.rcParams["svg.fonttype"] == "path": + if mpl.rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) else: self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext) if gc.get_url() is not None: - self.writer.end("a") + self.writer.end('a') if clip_attrs: - self.writer.end("g") + self.writer.end('g') def flipy(self): # docstring inherited @@ -1451,7 +1302,8 @@ def get_text_width_height_descent(self, s, prop, ismath): class FigureCanvasSVG(FigureCanvasBase): - filetypes = {"svg": "Scalable Vector Graphics", "svgz": "Scalable Vector Graphics"} + filetypes = {'svg': 'Scalable Vector Graphics', + 'svgz': 'Scalable Vector Graphics'} fixed_dpi = 72 @@ -1489,31 +1341,25 @@ def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None): """ with cbook.open_file_cm(filename, "w", encoding="utf-8") as fh: if not cbook.file_requires_unicode(fh): - fh = codecs.getwriter("utf-8")(fh) + fh = codecs.getwriter('utf-8')(fh) dpi = self.figure.dpi self.figure.dpi = 72 width, height = self.figure.get_size_inches() w, h = width * 72, height * 72 renderer = MixedModeRenderer( - self.figure, - width, - height, - dpi, + self.figure, width, height, dpi, RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata), - bbox_inches_restore=bbox_inches_restore, - ) + bbox_inches_restore=bbox_inches_restore) self.figure.draw(renderer) renderer.finalize() def print_svgz(self, filename, **kwargs): - with ( - cbook.open_file_cm(filename, "wb") as fh, - gzip.GzipFile(mode="w", fileobj=fh) as gzipwriter, - ): + with (cbook.open_file_cm(filename, "wb") as fh, + gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter): return self.print_svg(gzipwriter, **kwargs) def get_default_filetype(self): - return "svg" + return 'svg' def draw(self): self.figure.draw_without_rendering() diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 47fdb3d7f435..684e15cdf854 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -18,34 +18,21 @@ import numpy as np import matplotlib as mpl -from . import ( - _api, - _path, - artist, - cbook, - colorizer as mcolorizer, - colors as mcolors, - _docstring, - hatch as mhatch, - lines as mlines, - path as mpath, - transforms, -) +from . import (_api, _path, artist, cbook, colorizer as mcolorizer, colors as mcolors, + _docstring, hatch as mhatch, lines as mlines, path as mpath, transforms) from ._enums import JoinStyle, CapStyle # "color" is excluded; it is a compound setter, and its docstring differs # in LineCollection. -@_api.define_aliases( - { - "antialiased": ["antialiaseds", "aa"], - "edgecolor": ["edgecolors", "ec"], - "facecolor": ["facecolors", "fc"], - "linestyle": ["linestyles", "dashes", "ls"], - "linewidth": ["linewidths", "lw"], - "offset_transform": ["transOffset"], - } -) +@_api.define_aliases({ + "antialiased": ["antialiaseds", "aa"], + "edgecolor": ["edgecolors", "ec"], + "facecolor": ["facecolors", "fc"], + "linestyle": ["linestyles", "dashes", "ls"], + "linewidth": ["linewidths", "lw"], + "offset_transform": ["transOffset"], +}) class Collection(mcolorizer.ColorizingArtist): r""" Base class for Collections. Must be subclassed to be usable. @@ -76,7 +63,6 @@ class Collection(mcolorizer.ColorizingArtist): mappable will be used to set the ``facecolors`` and ``edgecolors``, ignoring those that were manually passed in. """ - #: Either a list of 3x3 arrays or an Nx3x3 array (representing N #: transforms), suitable for the `all_transforms` argument to #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`; @@ -90,28 +76,26 @@ class Collection(mcolorizer.ColorizingArtist): _edge_default = False @_docstring.interpd - def __init__( - self, - *, - edgecolors=None, - facecolors=None, - hatchcolors=None, - linewidths=None, - linestyles="solid", - capstyle=None, - joinstyle=None, - antialiaseds=None, - offsets=None, - offset_transform=None, - norm=None, # optional for ScalarMappable - cmap=None, # ditto - colorizer=None, - pickradius=5.0, - hatch=None, - urls=None, - zorder=1, - **kwargs, - ): + def __init__(self, *, + edgecolors=None, + facecolors=None, + hatchcolors=None, + linewidths=None, + linestyles='solid', + capstyle=None, + joinstyle=None, + antialiaseds=None, + offsets=None, + offset_transform=None, + norm=None, # optional for ScalarMappable + cmap=None, # ditto + colorizer=None, + pickradius=5.0, + hatch=None, + urls=None, + zorder=1, + **kwargs + ): """ Parameters ---------- @@ -195,7 +179,7 @@ def __init__( self._face_is_mapped = None self._edge_is_mapped = None self._mapped_colors = None # calculated in update_scalarmappable - self._hatch_linewidth = mpl.rcParams["hatch.linewidth"] + self._hatch_linewidth = mpl.rcParams['hatch.linewidth'] self.set_facecolor(facecolors) self.set_edgecolor(edgecolors) self.set_linewidth(linewidths) @@ -244,10 +228,10 @@ def get_offset_transform(self): """Return the `.Transform` instance used by this artist offset.""" if self._offset_transform is None: self._offset_transform = transforms.IdentityTransform() - elif not isinstance(self._offset_transform, transforms.Transform) and hasattr( - self._offset_transform, "_as_mpl_transform" - ): - self._offset_transform = self._offset_transform._as_mpl_transform(self.axes) + elif (not isinstance(self._offset_transform, transforms.Transform) + and hasattr(self._offset_transform, '_as_mpl_transform')): + self._offset_transform = \ + self._offset_transform._as_mpl_transform(self.axes) return self._offset_transform def set_offset_transform(self, offset_transform): @@ -279,10 +263,8 @@ def get_datalim(self, transData): transform = self.get_transform() offset_trf = self.get_offset_transform() - if not ( - isinstance(offset_trf, transforms.IdentityTransform) - or offset_trf.contains_branch(transData) - ): + if not (isinstance(offset_trf, transforms.IdentityTransform) + or offset_trf.contains_branch(transData)): # if the offsets are in some coords other than data, # then don't use them for autoscaling. return transforms.Bbox.null() @@ -310,12 +292,10 @@ def get_datalim(self, transData): offsets = offsets.filled(np.nan) # get_path_collection_extents handles nan but not masked arrays return mpath.get_path_collection_extents( - transform.get_affine() - transData, - paths, + transform.get_affine() - transData, paths, self.get_transforms(), offset_trf.transform_non_affine(offsets), - offset_trf.get_affine().frozen(), - ) + offset_trf.get_affine().frozen()) # NOTE: None is the default case where no offsets were passed in if self._offsets is not None: @@ -359,7 +339,8 @@ def _prepare_points(self): offsets = np.ma.column_stack([xs, ys]) if not transform.is_affine: - paths = [transform.transform_path_non_affine(path) for path in paths] + paths = [transform.transform_path_non_affine(path) + for path in paths] transform = transform.get_affine() if not offset_trf.is_affine: offsets = offset_trf.transform_non_affine(offsets) @@ -396,7 +377,6 @@ def draw(self, renderer): if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer - renderer = PathEffectRenderer(self.get_path_effects(), renderer) # If the collection is made up of a single shape/color/stroke, @@ -409,26 +389,19 @@ def draw(self, renderer): facecolors = self.get_facecolor() edgecolors = self.get_edgecolor() do_single_path_optimization = False - if ( - len(paths) == 1 - and len(trans) <= 1 - and len(facecolors) == 1 - and len(edgecolors) == 1 - and len(self._linewidths) == 1 - and all(ls[1] is None for ls in self._linestyles) - and len(self._antialiaseds) == 1 - and len(self._urls) == 1 - and self.get_hatch() is None - ): + if (len(paths) == 1 and len(trans) <= 1 and + len(facecolors) == 1 and len(edgecolors) == 1 and + len(self._linewidths) == 1 and + all(ls[1] is None for ls in self._linestyles) and + len(self._antialiaseds) == 1 and len(self._urls) == 1 and + self.get_hatch() is None): if len(trans): combined_transform = transforms.Affine2D(trans[0]) + transform else: combined_transform = transform extents = paths[0].get_extents(combined_transform) - if ( - extents.width < self.get_figure(root=True).bbox.width - and extents.height < self.get_figure(root=True).bbox.height - ): + if (extents.width < self.get_figure(root=True).bbox.width + and extents.height < self.get_figure(root=True).bbox.height): do_single_path_optimization = True if self._joinstyle: @@ -444,13 +417,8 @@ def draw(self, renderer): gc.set_antialiased(self._antialiaseds[0]) gc.set_url(self._urls[0]) renderer.draw_markers( - gc, - paths[0], - combined_transform.frozen(), - mpath.Path(offsets), - offset_trf, - tuple(facecolors[0]), - ) + gc, paths[0], combined_transform.frozen(), + mpath.Path(offsets), offset_trf, tuple(facecolors[0])) else: # The current new API of draw_path_collection() is provisional # and will be changed in a future PR. @@ -462,20 +430,12 @@ def draw(self, renderer): hatchcolors_arg_supported = True try: renderer.draw_path_collection( - gc, - transform.frozen(), - [], - self.get_transforms(), - offsets, - offset_trf, - self.get_facecolor(), - self.get_edgecolor(), - self._linewidths, - self._linestyles, - self._antialiaseds, - self._urls, - "screen", - hatchcolors=self.get_hatchcolor(), + gc, transform.frozen(), [], + self.get_transforms(), offsets, offset_trf, + self.get_facecolor(), self.get_edgecolor(), + self._linewidths, self._linestyles, + self._antialiaseds, self._urls, + "screen", hatchcolors=self.get_hatchcolor() ) except TypeError: # If the renderer does not support the hatchcolors argument, @@ -486,47 +446,29 @@ def draw(self, renderer): # If the hatchcolors argument is not needed or not passed # then we can skip the iteration over paths in case the # argument is not supported by the renderer. - hatchcolors_not_needed = ( - self.get_hatch() is None or self._original_hatchcolor is None - ) + hatchcolors_not_needed = (self.get_hatch() is None or + self._original_hatchcolor is None) if self._gapcolor is not None: # First draw paths within the gaps. ipaths, ilinestyles = self._get_inverse_paths_linestyles() - args = [ - offsets, - offset_trf, - [mcolors.to_rgba("none")], - self._gapcolor, - self._linewidths, - ilinestyles, - self._antialiaseds, - self._urls, - "screen", - ] + args = [offsets, offset_trf, [mcolors.to_rgba("none")], self._gapcolor, + self._linewidths, ilinestyles, self._antialiaseds, self._urls, + "screen"] if hatchcolors_arg_supported: - renderer.draw_path_collection( - gc, - transform.frozen(), - ipaths, - self.get_transforms(), - *args, - hatchcolors=self.get_hatchcolor(), - ) + renderer.draw_path_collection(gc, transform.frozen(), ipaths, + self.get_transforms(), *args, + hatchcolors=self.get_hatchcolor()) else: if hatchcolors_not_needed: - renderer.draw_path_collection( - gc, transform.frozen(), ipaths, self.get_transforms(), *args - ) + renderer.draw_path_collection(gc, transform.frozen(), ipaths, + self.get_transforms(), *args) else: path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), ipaths, self.get_transforms() - ) + transform.frozen(), ipaths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, - list(path_ids), - *args, + gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), ): path, transform = path_id @@ -535,41 +477,23 @@ def draw(self, renderer): transform.translate(xo, yo) renderer.draw_path(gc0, path, transform, rgbFace) - args = [ - offsets, - offset_trf, - self.get_facecolor(), - self.get_edgecolor(), - self._linewidths, - self._linestyles, - self._antialiaseds, - self._urls, - "screen", - ] + args = [offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), + self._linewidths, self._linestyles, self._antialiaseds, self._urls, + "screen"] if hatchcolors_arg_supported: - renderer.draw_path_collection( - gc, - transform.frozen(), - paths, - self.get_transforms(), - *args, - hatchcolors=self.get_hatchcolor(), - ) + renderer.draw_path_collection(gc, transform.frozen(), paths, + self.get_transforms(), *args, + hatchcolors=self.get_hatchcolor()) else: if hatchcolors_not_needed: - renderer.draw_path_collection( - gc, transform.frozen(), paths, self.get_transforms(), *args - ) + renderer.draw_path_collection(gc, transform.frozen(), paths, + self.get_transforms(), *args) else: path_ids = renderer._iter_collection_raw_paths( - transform.frozen(), paths, self.get_transforms() - ) + transform.frozen(), paths, self.get_transforms()) for xo, yo, path_id, gc0, rgbFace in renderer._iter_collection( - gc, - list(path_ids), - *args, - hatchcolors=self.get_hatchcolor(), + gc, list(path_ids), *args, hatchcolors=self.get_hatchcolor(), ): path, transform = path_id if xo != 0 or yo != 0: @@ -592,8 +516,7 @@ def set_pickradius(self, pickradius): """ if not isinstance(pickradius, Real): raise ValueError( - f"pickradius must be a real-valued number, not {pickradius!r}" - ) + f"pickradius must be a real-valued number, not {pickradius!r}") self._pickradius = pickradius def get_pickradius(self): @@ -610,10 +533,9 @@ def contains(self, mouseevent): return False, {} pickradius = ( float(self._picker) - if isinstance(self._picker, Number) - and self._picker is not True # the bool, not just nonzero or 1 - else self._pickradius - ) + if isinstance(self._picker, Number) and + self._picker is not True # the bool, not just nonzero or 1 + else self._pickradius) if self.axes: self.axes._unstale_viewLim() transform, offset_trf, offsets, paths = self._prepare_points() @@ -623,16 +545,9 @@ def contains(self, mouseevent): # following the path. If pickradius <= 0, then we instead simply check # if the point is *inside* of the path instead. ind = _path.point_in_path_collection( - mouseevent.x, - mouseevent.y, - pickradius, - transform.frozen(), - paths, - self.get_transforms(), - offsets, - offset_trf, - pickradius <= 0, - ) + mouseevent.x, mouseevent.y, pickradius, + transform.frozen(), paths, self.get_transforms(), + offsets, offset_trf, pickradius <= 0) return len(ind) > 0, dict(ind=ind) def set_urls(self, urls): @@ -715,17 +630,11 @@ def set_offsets(self, offsets): offsets = np.asanyarray(offsets) if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else. offsets = offsets[None, :] - cstack = ( - np.ma.column_stack - if isinstance(offsets, np.ma.MaskedArray) - else np.column_stack - ) + cstack = (np.ma.column_stack if isinstance(offsets, np.ma.MaskedArray) + else np.column_stack) self._offsets = cstack( - ( - np.asanyarray(self.convert_xunits(offsets[:, 0]), float), - np.asanyarray(self.convert_yunits(offsets[:, 1]), float), - ) - ) + (np.asanyarray(self.convert_xunits(offsets[:, 0]), float), + np.asanyarray(self.convert_yunits(offsets[:, 1]), float))) self.stale = True def get_offsets(self): @@ -735,7 +644,7 @@ def get_offsets(self): def _get_default_linewidth(self): # This may be overridden in a subclass. - return mpl.rcParams["patch.linewidth"] # validated as float + return mpl.rcParams['patch.linewidth'] # validated as float def set_linewidth(self, lw): """ @@ -754,8 +663,7 @@ def set_linewidth(self, lw): # scale all of the dash patterns. self._linewidths, self._linestyles = self._bcast_lwls( - self._us_lw, self._us_linestyles - ) + self._us_lw, self._us_linestyles) self.stale = True def set_linestyle(self, ls): @@ -789,8 +697,7 @@ def set_linestyle(self, ls): # broadcast and scale the lw and dash patterns self._linewidths, self._linestyles = self._bcast_lwls( - self._us_lw, self._us_linestyles - ) + self._us_lw, self._us_linestyles) @_docstring.interpd def set_capstyle(self, cs): @@ -858,7 +765,7 @@ def _bcast_lwls(linewidths, dashes): linewidths, dashes : list Will be the same length, dashes are scaled by paired linewidth """ - if mpl.rcParams["_internal.classic_mode"]: + if mpl.rcParams['_internal.classic_mode']: return linewidths, dashes # make sure they are the same length so we can zip them if len(dashes) != len(linewidths): @@ -869,9 +776,8 @@ def _bcast_lwls(linewidths, dashes): linewidths = list(linewidths) * (l_dashes // gcd) # scale the dash patterns - dashes = [ - mlines._scale_dashes(o, d, lw) for (o, d), lw in zip(dashes, linewidths) - ] + dashes = [mlines._scale_dashes(o, d, lw) + for (o, d), lw in zip(dashes, linewidths)] return linewidths, dashes @@ -900,7 +806,7 @@ def set_antialiased(self, aa): def _get_default_antialiased(self): # This may be overridden in a subclass. - return mpl.rcParams["patch.antialiased"] + return mpl.rcParams['patch.antialiased'] def set_color(self, c): """ @@ -924,7 +830,7 @@ def set_color(self, c): def _get_default_facecolor(self): # This may be overridden in a subclass. - return mpl.rcParams["patch.facecolor"] + return mpl.rcParams['patch.facecolor'] def _set_facecolor(self, c): if c is None: @@ -954,36 +860,33 @@ def get_facecolor(self): return self._facecolors def get_edgecolor(self): - if cbook._str_equal(self._edgecolors, "face"): + if cbook._str_equal(self._edgecolors, 'face'): return self.get_facecolor() else: return self._edgecolors def _get_default_edgecolor(self): # This may be overridden in a subclass. - return mpl.rcParams["patch.edgecolor"] + return mpl.rcParams['patch.edgecolor'] def get_hatchcolor(self): - if cbook._str_equal(self._hatchcolors, "edge"): + if cbook._str_equal(self._hatchcolors, 'edge'): if len(self.get_edgecolor()) == 0: - return mpl.colors.to_rgba_array( - self._get_default_edgecolor(), self._alpha - ) + return mpl.colors.to_rgba_array(self._get_default_edgecolor(), + self._alpha) return self.get_edgecolor() return self._hatchcolors def _set_edgecolor(self, c): if c is None: - if ( - mpl.rcParams["patch.force_edgecolor"] - or self._edge_default - or cbook._str_equal(self._original_facecolor, "none") - ): + if (mpl.rcParams['patch.force_edgecolor'] + or self._edge_default + or cbook._str_equal(self._original_facecolor, 'none')): c = self._get_default_edgecolor() else: - c = "none" - if cbook._str_lower_equal(c, "face"): - self._edgecolors = "face" + c = 'none' + if cbook._str_lower_equal(c, 'face'): + self._edgecolors = 'face' self.stale = True return self._edgecolors = mcolors.to_rgba_array(c, self._alpha) @@ -1008,9 +911,9 @@ def set_edgecolor(self, c): self._set_edgecolor(c) def _set_hatchcolor(self, c): - c = mpl._val_or_rc(c, "hatch.color") - if cbook._str_equal(c, "edge"): - self._hatchcolors = "edge" + c = mpl._val_or_rc(c, 'hatch.color') + if cbook._str_equal(c, 'edge'): + self._hatchcolors = 'edge' else: self._hatchcolors = mcolors.to_rgba_array(c, self._alpha) self.stale = True @@ -1073,21 +976,18 @@ def _set_mappable_flags(self): self._edge_is_mapped = False self._face_is_mapped = False if self._A is not None: - if not cbook._str_equal(self._original_facecolor, "none"): + if not cbook._str_equal(self._original_facecolor, 'none'): self._face_is_mapped = True - if cbook._str_equal(self._original_edgecolor, "face"): + if cbook._str_equal(self._original_edgecolor, 'face'): self._edge_is_mapped = True else: if self._original_edgecolor is None: self._edge_is_mapped = True mapped = self._face_is_mapped or self._edge_is_mapped - changed = ( - edge0 is None - or face0 is None - or self._edge_is_mapped != edge0 - or self._face_is_mapped != face0 - ) + changed = (edge0 is None or face0 is None + or self._edge_is_mapped != edge0 + or self._face_is_mapped != face0) return mapped or changed def update_scalarmappable(self): @@ -1103,18 +1003,17 @@ def update_scalarmappable(self): if self._A is not None: # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array) if self._A.ndim > 1 and not isinstance(self, _MeshData): - raise ValueError("Collections can only map rank 1 arrays") + raise ValueError('Collections can only map rank 1 arrays') if np.iterable(self._alpha): if self._alpha.size != self._A.size: raise ValueError( - f"Data array shape, {self._A.shape} " - "is incompatible with alpha array shape, " - f"{self._alpha.shape}. " - "This can occur with the deprecated " + f'Data array shape, {self._A.shape} ' + 'is incompatible with alpha array shape, ' + f'{self._alpha.shape}. ' + 'This can occur with the deprecated ' 'behavior of the "flat" shading option, ' - "in which a row and/or column of the data " - "array is dropped." - ) + 'in which a row and/or column of the data ' + 'array is dropped.') # pcolormesh, scatter, maybe others flatten their _A self._alpha = self._alpha.reshape(self._A.shape) self._mapped_colors = self.to_rgba(self._A, self._alpha) @@ -1163,7 +1062,6 @@ class _CollectionWithSizes(Collection): """ Base class for collections that have an array of sizes. """ - _factor = 1.0 def get_sizes(self): @@ -1234,9 +1132,8 @@ def __init__(self, paths, sizes=None, **kwargs): def get_paths(self): return self._paths - def legend_elements( - self, prop="colors", num="auto", fmt=None, func=lambda x: x, **kwargs - ): + def legend_elements(self, prop="colors", num="auto", + fmt=None, func=lambda x: x, **kwargs): """ Create legend handles and labels for a PathCollection. @@ -1309,11 +1206,9 @@ def legend_elements( if prop == "colors": if not hasarray: - warnings.warn( - "Collection without array used. Make sure to " - "specify the values to be colormapped via the " - "`c` argument." - ) + warnings.warn("Collection without array used. Make sure to " + "specify the values to be colormapped via the " + "`c` argument.") return handles, labels u = np.unique(self.get_array()) size = kwargs.pop("size", mpl.rcParams["lines.markersize"]) @@ -1321,10 +1216,8 @@ def legend_elements( u = np.unique(self.get_sizes()) color = kwargs.pop("color", "k") else: - raise ValueError( - "Valid values for `prop` are 'colors' or " - f"'sizes'. You supplied '{prop}' instead." - ) + raise ValueError("Valid values for `prop` are 'colors' or " + f"'sizes'. You supplied '{prop}' instead.") fu = func(u) fmt.axis.set_view_interval(fu.min(), fu.max()) @@ -1347,22 +1240,20 @@ def legend_elements( loc = mpl.ticker.FixedLocator(num) else: num = int(num) - loc = mpl.ticker.MaxNLocator( - nbins=num, min_n_ticks=num - 1, steps=[1, 2, 2.5, 3, 5, 6, 8, 10] - ) + loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1, + steps=[1, 2, 2.5, 3, 5, 6, 8, 10]) label_values = loc.tick_values(func(arr).min(), func(arr).max()) - cond = (label_values >= func(arr).min()) & (label_values <= func(arr).max()) + cond = ((label_values >= func(arr).min()) & + (label_values <= func(arr).max())) label_values = label_values[cond] yarr = np.linspace(arr.min(), arr.max(), 256) xarr = func(yarr) ix = np.argsort(xarr) values = np.interp(label_values, xarr[ix], yarr[ix]) - kw = { - "markeredgewidth": self.get_linewidths()[0], - "alpha": self.get_alpha(), - **kwargs, - } + kw = {"markeredgewidth": self.get_linewidths()[0], + "alpha": self.get_alpha(), + **kwargs} for val, lab in zip(values, label_values): if prop == "colors": @@ -1371,9 +1262,8 @@ def legend_elements( size = np.sqrt(val) if np.isclose(size, 0.0): continue - h = mlines.Line2D( - [0], [0], ls="", color=color, ms=size, marker=self.get_paths()[0], **kw - ) + h = mlines.Line2D([0], [0], ls="", color=color, ms=size, + marker=self.get_paths()[0], **kw) handles.append(h) if hasattr(fmt, "set_locs"): fmt.set_locs(label_values) @@ -1438,29 +1328,17 @@ def set_verts(self, verts, closed=True): verts_pad = np.concatenate((verts, verts[:, :1]), axis=1) # It's faster to create the codes and internal flags once in a # template path and reuse them. - if closed and verts.shape[1] >= 128: - template_path = mpath.Path(verts_pad[0], closed=False) - else: - template_path = mpath.Path(verts_pad[0], closed=True) + template_path = mpath.Path(verts_pad[0], closed=True) codes = template_path.codes _make_path = mpath.Path._fast_from_codes_and_verts - self._paths = [ - _make_path(xy, codes, internals_from=template_path) for xy in verts_pad - ] + self._paths = [_make_path(xy, codes, internals_from=template_path) + for xy in verts_pad] return self._paths = [] for xy in verts: if len(xy): - if closed and len(xy) >= 128: - if isinstance(xy, np.ma.MaskedArray): - xy = xy.filled(np.nan) - xy = np.asanyarray(xy) - if len(xy) and (xy[0] != xy[-1]).any(): - xy = np.concatenate([xy, xy[:1]]) - self._paths.append(mpath.Path(xy, closed=False)) - else: - self._paths.append(mpath.Path._create_closed(xy)) + self._paths.append(mpath.Path._create_closed(xy)) else: self._paths.append(mpath.Path(xy)) @@ -1469,13 +1347,10 @@ def set_verts(self, verts, closed=True): def set_verts_and_codes(self, verts, codes): """Initialize vertices with path codes.""" if len(verts) != len(codes): - raise ValueError( - "'codes' must be a 1D list or array " "with the same length of 'verts'" - ) - self._paths = [ - mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) - for xy, cds in zip(verts, codes) - ] + raise ValueError("'codes' must be a 1D list or array " + "with the same length of 'verts'") + self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) + for xy, cds in zip(verts, codes)] self.stale = True @@ -1483,19 +1358,9 @@ class FillBetweenPolyCollection(PolyCollection): """ `.PolyCollection` that fills the area between two x- or y-curves. """ - def __init__( - self, - t_direction, - t, - f1, - f2, - *, - where=None, - interpolate=False, - step=None, - **kwargs, - ): + self, t_direction, t, f1, f2, *, + where=None, interpolate=False, step=None, **kwargs): """ Parameters ---------- @@ -1608,8 +1473,7 @@ def set_data(self, t, f1, f2, *, where=None): .PolyCollection.set_verts, .Line2D.set_data """ t, f1, f2 = self.axes._fill_between_process_units( - self.t_direction, self._f_direction, t, f1, f2 - ) + self.t_direction, self._f_direction, t, f1, f2) verts = self._make_verts(t, f1, f2, where) self.set_verts(verts) @@ -1617,11 +1481,8 @@ def set_data(self, t, f1, f2, *, where=None): def get_datalim(self, transData): """Calculate the data limits and return them as a `.Bbox`.""" datalim = transforms.Bbox.null() - datalim.update_from_data_xy( - (self.get_transform() - transData).transform( - np.concatenate([self._bbox, [self._bbox.minpos]]) - ) - ) + datalim.update_from_data_xy((self.get_transform() - transData).transform( + np.concatenate([self._bbox, [self._bbox.minpos]]))) return datalim def _make_verts(self, t, f1, f2, where): @@ -1634,13 +1495,8 @@ def _make_verts(self, t, f1, f2, where): t, f1, f2 = np.broadcast_arrays(np.atleast_1d(t), f1, f2, subok=True) self._bbox = transforms.Bbox.null() - self._bbox.update_from_data_xy( - self._fix_pts_xy_order( - np.concatenate( - [np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)] - ) - ) - ) + self._bbox.update_from_data_xy(self._fix_pts_xy_order(np.concatenate([ + np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)]))) return [ self._make_verts_for_region(t, f1, f2, idx0, idx1) @@ -1660,12 +1516,10 @@ def _get_data_mask(self, t, f1, f2, where): where = np.asarray(where, dtype=bool) if where.size != t.size: msg = "where size ({}) does not match {!r} size ({})".format( - where.size, self.t_direction, t.size - ) + where.size, self.t_direction, t.size) raise ValueError(msg) return where & ~functools.reduce( - np.logical_or, map(np.ma.getmaskarray, [t, f1, f2]) - ) + np.logical_or, map(np.ma.getmaskarray, [t, f1, f2])) @staticmethod def _validate_shapes(t_dir, f_dir, t, f1, f2): @@ -1676,8 +1530,7 @@ def _validate_shapes(t_dir, f_dir, t, f1, f2): raise ValueError(f"{name!r} is not 1-dimensional") if t.size > 1 and array.size > 1 and t.size != array.size: msg = "{!r} has size {}, but {!r} has an unequal size of {}".format( - t_dir, t.size, name, array.size - ) + t_dir, t.size, name, array.size) raise ValueError(msg) def _make_verts_for_region(self, t, f1, f2, idx0, idx1): @@ -1701,14 +1554,11 @@ def _make_verts_for_region(self, t, f1, f2, idx0, idx1): start = t_slice[0], f2_slice[0] end = t_slice[-1], f2_slice[-1] - pts = np.concatenate( - ( - np.asarray([start]), - np.stack((t_slice, f1_slice), axis=-1), - np.asarray([end]), - np.stack((t_slice, f2_slice), axis=-1)[::-1], - ) - ) + pts = np.concatenate(( + np.asarray([start]), + np.stack((t_slice, f1_slice), axis=-1), + np.asarray([end]), + np.stack((t_slice, f2_slice), axis=-1)[::-1])) return self._fix_pts_xy_order(pts) @@ -1716,9 +1566,9 @@ def _make_verts_for_region(self, t, f1, f2, idx0, idx1): def _get_interpolating_points(cls, t, f1, f2, idx): """Calculate interpolating points.""" im1 = max(idx - 1, 0) - t_values = t[im1 : idx + 1] - diff_values = f1[im1 : idx + 1] - f2[im1 : idx + 1] - f1_values = f1[im1 : idx + 1] + t_values = t[im1:idx+1] + diff_values = f1[im1:idx+1] - f2[im1:idx+1] + f1_values = f1[im1:idx+1] if len(diff_values) == 2: if np.ma.is_masked(diff_values[1]): @@ -1750,9 +1600,14 @@ class RegularPolyCollection(_CollectionWithSizes): """A collection of n-sided regular polygons.""" _path_generator = mpath.Path.unit_regular_polygon - _factor = np.pi ** (-1 / 2) + _factor = np.pi ** (-1/2) - def __init__(self, numsides, *, rotation=0, sizes=(1,), **kwargs): + def __init__(self, + numsides, + *, + rotation=0, + sizes=(1,), + **kwargs): """ Parameters ---------- @@ -1809,13 +1664,11 @@ def draw(self, renderer): class StarPolygonCollection(RegularPolyCollection): """Draw a collection of regular stars with *numsides* points.""" - _path_generator = mpath.Path.unit_regular_star class AsteriskPolygonCollection(RegularPolyCollection): """Draw a collection of regular asterisks with *numsides* points.""" - _path_generator = mpath.Path.unit_regular_asterisk @@ -1839,9 +1692,11 @@ class LineCollection(Collection): _edge_default = True - def __init__( - self, segments, *, zorder=2, **kwargs # Can be None. # Collection.zorder is 1 - ): + def __init__(self, segments, # Can be None. + *, + zorder=2, # Collection.zorder is 1 + **kwargs + ): """ Parameters ---------- @@ -1875,22 +1730,19 @@ def __init__( Forwarded to `.Collection`. """ # Unfortunately, mplot3d needs this explicit setting of 'facecolors'. - kwargs.setdefault("facecolors", "none") - super().__init__(zorder=zorder, **kwargs) + kwargs.setdefault('facecolors', 'none') + super().__init__( + zorder=zorder, + **kwargs) self.set_segments(segments) def set_segments(self, segments): if segments is None: return - self._paths = [ - ( - mpath.Path(seg) - if isinstance(seg, np.ma.MaskedArray) - else mpath.Path(np.asarray(seg, float)) - ) - for seg in segments - ] + self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray) + else mpath.Path(np.asarray(seg, float)) + for seg in segments] self.stale = True set_verts = set_segments # for compatibility with PolyCollection @@ -1921,16 +1773,16 @@ def get_segments(self): return segments def _get_default_linewidth(self): - return mpl.rcParams["lines.linewidth"] + return mpl.rcParams['lines.linewidth'] def _get_default_antialiased(self): - return mpl.rcParams["lines.antialiased"] + return mpl.rcParams['lines.antialiased'] def _get_default_edgecolor(self): - return mpl.rcParams["lines.color"] + return mpl.rcParams['lines.color'] def _get_default_facecolor(self): - return "none" + return 'none' def set_alpha(self, alpha): # docstring inherited @@ -1997,13 +1849,11 @@ def _get_inverse_paths_linestyles(self): to nans to prevent drawing an inverse line. """ path_patterns = [ - ( - (mpath.Path(np.full((1, 2), np.nan)), ls) - if ls == (0, None) - else (path, mlines._get_inverse_dash_pattern(*ls)) - ) - for (path, ls) in zip(self._paths, itertools.cycle(self._linestyles)) - ] + (mpath.Path(np.full((1, 2), np.nan)), ls) + if ls == (0, None) else + (path, mlines._get_inverse_dash_pattern(*ls)) + for (path, ls) in + zip(self._paths, itertools.cycle(self._linestyles))] return zip(*path_patterns) @@ -2018,19 +1868,18 @@ class EventCollection(LineCollection): _edge_default = True - def __init__( - self, - positions, # Cannot be None. - orientation="horizontal", - *, - lineoffset=0, - linelength=1, - linewidth=None, - color=None, - linestyle="solid", - antialiased=None, - **kwargs, - ): + def __init__(self, + positions, # Cannot be None. + orientation='horizontal', + *, + lineoffset=0, + linelength=1, + linewidth=None, + color=None, + linestyle='solid', + antialiased=None, + **kwargs + ): """ Parameters ---------- @@ -2067,14 +1916,10 @@ def __init__( -------- .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py """ - super().__init__( - [], - linewidths=linewidth, - linestyles=linestyle, - colors=color, - antialiaseds=antialiased, - **kwargs, - ) + super().__init__([], + linewidths=linewidth, linestyles=linestyle, + colors=color, antialiaseds=antialiased, + **kwargs) self._is_horizontal = True # Initial value, may be switched below. self._linelength = linelength self._lineoffset = lineoffset @@ -2093,7 +1938,7 @@ def set_positions(self, positions): if positions is None: positions = [] if np.ndim(positions) != 1: - raise ValueError("positions must be one-dimensional") + raise ValueError('positions must be one-dimensional') lineoffset = self.get_lineoffset() linelength = self.get_linelength() pos_idx = 0 if self.is_horizontal() else 1 @@ -2105,12 +1950,12 @@ def set_positions(self, positions): def add_positions(self, position): """Add one or more events at the specified positions.""" - if position is None or (hasattr(position, "len") and len(position) == 0): + if position is None or (hasattr(position, 'len') and + len(position) == 0): return positions = self.get_positions() positions = np.hstack([positions, np.asanyarray(position)]) self.set_positions(positions) - extend_positions = append_positions = add_positions def is_horizontal(self): @@ -2121,7 +1966,7 @@ def get_orientation(self): """ Return the orientation of the event line ('horizontal' or 'vertical'). """ - return "horizontal" if self.is_horizontal() else "vertical" + return 'horizontal' if self.is_horizontal() else 'vertical' def switch_orientation(self): """ @@ -2144,8 +1989,8 @@ def set_orientation(self, orientation): orientation : {'horizontal', 'vertical'} """ is_horizontal = _api.check_getitem( - {"horizontal": True, "vertical": False}, orientation=orientation - ) + {"horizontal": True, "vertical": False}, + orientation=orientation) if is_horizontal == self.is_horizontal(): return self.switch_orientation() @@ -2162,8 +2007,8 @@ def set_linelength(self, linelength): segments = self.get_segments() pos = 1 if self.is_horizontal() else 0 for segment in segments: - segment[0, pos] = lineoffset + linelength / 2.0 - segment[1, pos] = lineoffset - linelength / 2.0 + segment[0, pos] = lineoffset + linelength / 2. + segment[1, pos] = lineoffset - linelength / 2. self.set_segments(segments) self._linelength = linelength @@ -2179,8 +2024,8 @@ def set_lineoffset(self, lineoffset): segments = self.get_segments() pos = 1 if self.is_horizontal() else 0 for segment in segments: - segment[0, pos] = lineoffset + linelength / 2.0 - segment[1, pos] = lineoffset - linelength / 2.0 + segment[0, pos] = lineoffset + linelength / 2. + segment[1, pos] = lineoffset - linelength / 2. self.set_segments(segments) self._lineoffset = lineoffset @@ -2199,7 +2044,7 @@ def get_color(self): class CircleCollection(_CollectionWithSizes): """A collection of circles, drawn using splines.""" - _factor = np.pi ** (-1 / 2) + _factor = np.pi ** (-1/2) def __init__(self, sizes, **kwargs): """ @@ -2219,7 +2064,7 @@ def __init__(self, sizes, **kwargs): class EllipseCollection(Collection): """A collection of ellipses, drawn using splines.""" - def __init__(self, widths, heights, angles, *, units="points", **kwargs): + def __init__(self, widths, heights, angles, *, units='points', **kwargs): """ Parameters ---------- @@ -2255,24 +2100,24 @@ def _set_transforms(self): ax = self.axes fig = self.get_figure(root=False) - if self._units == "xy": + if self._units == 'xy': sc = 1 - elif self._units == "x": + elif self._units == 'x': sc = ax.bbox.width / ax.viewLim.width - elif self._units == "y": + elif self._units == 'y': sc = ax.bbox.height / ax.viewLim.height - elif self._units == "inches": + elif self._units == 'inches': sc = fig.dpi - elif self._units == "points": + elif self._units == 'points': sc = fig.dpi / 72.0 - elif self._units == "width": + elif self._units == 'width': sc = ax.bbox.width - elif self._units == "height": + elif self._units == 'height': sc = ax.bbox.height - elif self._units == "dots": + elif self._units == 'dots': sc = 1.0 else: - raise ValueError(f"Unrecognized units: {self._units!r}") + raise ValueError(f'Unrecognized units: {self._units!r}') self._transforms = np.zeros((len(self._widths), 3, 3)) widths = self._widths * sc @@ -2286,7 +2131,7 @@ def _set_transforms(self): self._transforms[:, 2, 2] = 1.0 _affine = transforms.Affine2D - if self._units == "xy": + if self._units == 'xy': m = ax.transData.get_affine().get_matrix().copy() m[:2, 2:] = 0 self.set_transform(_affine(m)) @@ -2363,24 +2208,24 @@ def __init__(self, patches, *, match_original=False, **kwargs): """ if match_original: - def determine_facecolor(patch): if patch.get_fill(): return patch.get_facecolor() return [0, 0, 0, 0] - kwargs["facecolors"] = [determine_facecolor(p) for p in patches] - kwargs["edgecolors"] = [p.get_edgecolor() for p in patches] - kwargs["linewidths"] = [p.get_linewidth() for p in patches] - kwargs["linestyles"] = [p.get_linestyle() for p in patches] - kwargs["antialiaseds"] = [p.get_antialiased() for p in patches] + kwargs['facecolors'] = [determine_facecolor(p) for p in patches] + kwargs['edgecolors'] = [p.get_edgecolor() for p in patches] + kwargs['linewidths'] = [p.get_linewidth() for p in patches] + kwargs['linestyles'] = [p.get_linestyle() for p in patches] + kwargs['antialiaseds'] = [p.get_antialiased() for p in patches] super().__init__(**kwargs) self.set_paths(patches) def set_paths(self, patches): - paths = [p.get_transform().transform_path(p.get_path()) for p in patches] + paths = [p.get_transform().transform_path(p.get_path()) + for p in patches] self._paths = paths @@ -2390,17 +2235,17 @@ class TriMesh(Collection): A triangular mesh is a `~matplotlib.tri.Triangulation` object. """ - def __init__(self, triangulation, **kwargs): super().__init__(**kwargs) self._triangulation = triangulation - self._shading = "gouraud" + self._shading = 'gouraud' self._bbox = transforms.Bbox.unit() # Unfortunately this requires a copy, unless Triangulation # was rewritten. - xy = np.hstack((triangulation.x.reshape(-1, 1), triangulation.y.reshape(-1, 1))) + xy = np.hstack((triangulation.x.reshape(-1, 1), + triangulation.y.reshape(-1, 1))) self._bbox.update_from_data_xy(xy) def get_paths(self): @@ -2473,8 +2318,7 @@ class _MeshData: shading : {'flat', 'gouraud'}, default: 'flat' """ - - def __init__(self, coordinates, *, shading="flat"): + def __init__(self, coordinates, *, shading='flat'): _api.check_shape((None, None, 2), coordinates=coordinates) self._coordinates = coordinates self._shading = shading @@ -2502,7 +2346,7 @@ def set_array(self, A): shading. """ height, width = self._coordinates.shape[0:-1] - if self._shading == "flat": + if self._shading == 'flat': h, w = height - 1, width - 1 else: h, w = height, width @@ -2513,8 +2357,7 @@ def set_array(self, A): raise ValueError( f"For X ({width}) and Y ({height}) with {self._shading} " f"shading, A should have shape " - f"{' or '.join(map(str, ok_shapes))}, not {A.shape}" - ) + f"{' or '.join(map(str, ok_shapes))}, not {A.shape}") return super().set_array(A) def get_coordinates(self): @@ -2551,9 +2394,13 @@ def _convert_mesh_to_paths(coordinates): c = coordinates.data else: c = coordinates - points = np.concatenate( - [c[:-1, :-1], c[:-1, 1:], c[1:, 1:], c[1:, :-1], c[:-1, :-1]], axis=2 - ).reshape((-1, 5, 2)) + points = np.concatenate([ + c[:-1, :-1], + c[:-1, 1:], + c[1:, 1:], + c[1:, :-1], + c[:-1, :-1] + ], axis=2).reshape((-1, 5, 2)) return [mpath.Path(x) for x in points] def _convert_mesh_to_triangles(self, coordinates): @@ -2572,23 +2419,12 @@ def _convert_mesh_to_triangles(self, coordinates): p_c = p[1:, 1:] p_d = p[1:, :-1] p_center = (p_a + p_b + p_c + p_d) / 4.0 - triangles = np.concatenate( - [ - p_a, - p_b, - p_center, - p_b, - p_c, - p_center, - p_c, - p_d, - p_center, - p_d, - p_a, - p_center, - ], - axis=2, - ).reshape((-1, 3, 2)) + triangles = np.concatenate([ + p_a, p_b, p_center, + p_b, p_c, p_center, + p_c, p_d, p_center, + p_d, p_a, p_center, + ], axis=2).reshape((-1, 3, 2)) c = self.get_facecolor().reshape((*coordinates.shape[:2], 4)) z = self.get_array() @@ -2600,23 +2436,12 @@ def _convert_mesh_to_triangles(self, coordinates): c_c = c[1:, 1:] c_d = c[1:, :-1] c_center = (c_a + c_b + c_c + c_d) / 4.0 - colors = np.concatenate( - [ - c_a, - c_b, - c_center, - c_b, - c_c, - c_center, - c_c, - c_d, - c_center, - c_d, - c_a, - c_center, - ], - axis=2, - ).reshape((-1, 3, 4)) + colors = np.concatenate([ + c_a, c_b, c_center, + c_b, c_c, c_center, + c_c, c_d, c_center, + c_d, c_a, c_center, + ], axis=2).reshape((-1, 3, 4)) tmask = np.isnan(colors[..., 2, 3]) return triangles[~tmask], colors[~tmask] @@ -2655,7 +2480,8 @@ class QuadMesh(_MeshData, Collection): """ - def __init__(self, coordinates, *, antialiased=True, shading="flat", **kwargs): + def __init__(self, coordinates, *, antialiased=True, shading='flat', + **kwargs): kwargs.setdefault("pickradius", 0) super().__init__(coordinates=coordinates, shading=shading) Collection.__init__(self, **kwargs) @@ -2710,23 +2536,18 @@ def draw(self, renderer): self._set_gc_clip(gc) gc.set_linewidth(self.get_linewidth()[0]) - if self._shading == "gouraud": + if self._shading == 'gouraud': triangles, colors = self._convert_mesh_to_triangles(coordinates) - renderer.draw_gouraud_triangles(gc, triangles, colors, transform.frozen()) + renderer.draw_gouraud_triangles( + gc, triangles, colors, transform.frozen()) else: renderer.draw_quad_mesh( - gc, - transform.frozen(), - coordinates.shape[1] - 1, - coordinates.shape[0] - 1, - coordinates, - offsets, - offset_trf, + gc, transform.frozen(), + coordinates.shape[1] - 1, coordinates.shape[0] - 1, + coordinates, offsets, offset_trf, # Backends expect flattened rgba arrays (n*m, 4) for fc and ec self.get_facecolor().reshape((-1, 4)), - self._antialiased, - self.get_edgecolors().reshape((-1, 4)), - ) + self._antialiased, self.get_edgecolors().reshape((-1, 4))) gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False @@ -2785,7 +2606,7 @@ def _get_unmasked_polys(self): mask = np.any(np.ma.getmaskarray(self._coordinates), axis=-1) # We want the shape of the polygon, which is the corner of each X/Y array - mask = mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1] + mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1]) arr = self.get_array() if arr is not None: arr = np.ma.getmaskarray(arr) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 9216f11b0675..f65ade669167 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -76,31 +76,27 @@ class Path: made up front in the constructor that will not change when the data changes. """ - code_type = np.uint8 # Path codes - STOP = code_type(0) # 1 vertex - MOVETO = code_type(1) # 1 vertex - LINETO = code_type(2) # 1 vertex - CURVE3 = code_type(3) # 2 vertices - CURVE4 = code_type(4) # 3 vertices - CLOSEPOLY = code_type(79) # 1 vertex + STOP = code_type(0) # 1 vertex + MOVETO = code_type(1) # 1 vertex + LINETO = code_type(2) # 1 vertex + CURVE3 = code_type(3) # 2 vertices + CURVE4 = code_type(4) # 3 vertices + CLOSEPOLY = code_type(79) # 1 vertex #: A dictionary mapping Path codes to the number of vertices that the #: code expects. - NUM_VERTICES_FOR_CODE = { - STOP: 1, - MOVETO: 1, - LINETO: 1, - CURVE3: 2, - CURVE4: 3, - CLOSEPOLY: 1, - } - - def __init__( - self, vertices, codes=None, _interpolation_steps=1, closed=False, readonly=False - ): + NUM_VERTICES_FOR_CODE = {STOP: 1, + MOVETO: 1, + LINETO: 1, + CURVE3: 2, + CURVE4: 3, + CLOSEPOLY: 1} + + def __init__(self, vertices, codes=None, _interpolation_steps=1, + closed=False, readonly=False): """ Create a new path with the given vertices and codes. @@ -135,18 +131,14 @@ def __init__( if codes is not None and len(vertices): codes = np.asarray(codes, self.code_type) if codes.ndim != 1 or len(codes) != len(vertices): - raise ValueError( - "'codes' must be a 1D list or array with the " - "same length of 'vertices'. " - f"Your vertices have shape {vertices.shape} " - f"but your codes have shape {codes.shape}" - ) + raise ValueError("'codes' must be a 1D list or array with the " + "same length of 'vertices'. " + f"Your vertices have shape {vertices.shape} " + f"but your codes have shape {codes.shape}") if len(codes) and codes[0] != self.MOVETO: - raise ValueError( - "The first element of 'code' must be equal " - f"to 'MOVETO' ({self.MOVETO}). " - f"Your first code is {codes[0]}" - ) + raise ValueError("The first element of 'code' must be equal " + f"to 'MOVETO' ({self.MOVETO}). " + f"Your first code is {codes[0]}") elif closed and len(vertices): codes = np.empty(len(vertices), dtype=self.code_type) codes[0] = self.MOVETO @@ -191,7 +183,7 @@ def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): pth._interpolation_steps = internals_from._interpolation_steps else: pth._should_simplify = True - pth._simplify_threshold = mpl.rcParams["path.simplify_threshold"] + pth._simplify_threshold = mpl.rcParams['path.simplify_threshold'] pth._interpolation_steps = 1 return pth @@ -207,12 +199,12 @@ def _create_closed(cls, vertices): return cls(np.concatenate([v, v[:1]]), closed=True) def _update_values(self): - self._simplify_threshold = mpl.rcParams["path.simplify_threshold"] + self._simplify_threshold = mpl.rcParams['path.simplify_threshold'] self._should_simplify = ( - self._simplify_threshold > 0 - and mpl.rcParams["path.simplify"] - and len(self._vertices) >= 128 - and (self._codes is None or np.all(self._codes <= Path.LINETO)) + self._simplify_threshold > 0 and + mpl.rcParams['path.simplify'] and + len(self._vertices) >= 128 and + (self._codes is None or np.all(self._codes <= Path.LINETO)) ) @property @@ -358,9 +350,9 @@ def make_compound_path(cls, *args): if path.codes is None: if size: codes[i] = cls.MOVETO - codes[i + 1 : i + size] = cls.LINETO + codes[i+1:i+size] = cls.LINETO else: - codes[i : i + size] = path.codes + codes[i:i+size] = path.codes i += size not_stop_mask = codes != cls.STOP # Remove STOPs, as internal STOPs are a bug. return cls(vertices[not_stop_mask], codes[not_stop_mask]) @@ -371,17 +363,9 @@ def __repr__(self): def __len__(self): return len(self.vertices) - def iter_segments( - self, - transform=None, - remove_nans=True, - clip=None, - snap=False, - stroke_width=1.0, - simplify=None, - curves=True, - sketch=None, - ): + def iter_segments(self, transform=None, remove_nans=True, clip=None, + snap=False, stroke_width=1.0, simplify=None, + curves=True, sketch=None): """ Iterate over all curve segments in the path. @@ -423,16 +407,11 @@ def iter_segments( if not len(self): return - cleaned = self.cleaned( - transform=transform, - remove_nans=remove_nans, - clip=clip, - snap=snap, - stroke_width=stroke_width, - simplify=simplify, - curves=curves, - sketch=sketch, - ) + cleaned = self.cleaned(transform=transform, + remove_nans=remove_nans, clip=clip, + snap=snap, stroke_width=stroke_width, + simplify=simplify, curves=curves, + sketch=sketch) # Cache these object lookups for performance in the loop. NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE @@ -484,11 +463,11 @@ def iter_bezier(self, **kwargs): elif code == Path.LINETO: # "CURVE2" yield BezierSegment(np.array([prev_vert, verts])), code elif code == Path.CURVE3: - yield BezierSegment(np.array([prev_vert, verts[:2], verts[2:]])), code + yield BezierSegment(np.array([prev_vert, verts[:2], + verts[2:]])), code elif code == Path.CURVE4: - yield BezierSegment( - np.array([prev_vert, verts[:2], verts[2:4], verts[4:]]) - ), code + yield BezierSegment(np.array([prev_vert, verts[:2], + verts[2:4], verts[4:]])), code elif code == Path.CLOSEPOLY: yield BezierSegment(np.array([prev_vert, first_vert])), code elif code == Path.STOP: @@ -505,21 +484,11 @@ def _iter_connected_components(self): idxs = np.append((self.codes == Path.MOVETO).nonzero()[0], len(self.codes)) for sl in map(slice, idxs, idxs[1:]): yield Path._fast_from_codes_and_verts( - self.vertices[sl], self.codes[sl], self - ) - - def cleaned( - self, - transform=None, - remove_nans=False, - clip=None, - *, - simplify=False, - curves=False, - stroke_width=1.0, - snap=False, - sketch=None, - ): + self.vertices[sl], self.codes[sl], self) + + def cleaned(self, transform=None, remove_nans=False, clip=None, + *, simplify=False, curves=False, + stroke_width=1.0, snap=False, sketch=None): """ Return a new `Path` with vertices and codes cleaned according to the parameters. @@ -529,16 +498,8 @@ def cleaned( Path.iter_segments : for details of the keyword arguments. """ vertices, codes = _path.cleanup_path( - self, - transform, - remove_nans, - clip, - snap, - stroke_width, - simplify, - curves, - sketch, - ) + self, transform, remove_nans, clip, snap, stroke_width, simplify, + curves, sketch) pth = Path._fast_from_codes_and_verts(vertices, codes, self) if not simplify: pth._should_simplify = False @@ -554,9 +515,8 @@ def transformed(self, transform): A specialized path class that will cache the transformed result and automatically update when the transform changes. """ - return Path( - transform.transform(self.vertices), self.codes, self._interpolation_steps - ) + return Path(transform.transform(self.vertices), self.codes, + self._interpolation_steps) def contains_point(self, point, transform=None, radius=0.0): """ @@ -650,7 +610,7 @@ def contains_points(self, points, transform=None, radius=0.0): if transform is not None: transform = transform.frozen() result = _path.points_in_path(points, radius, self, transform) - return result.astype("bool") + return result.astype('bool') def contains_path(self, path, transform=None): """ @@ -680,7 +640,6 @@ def get_extents(self, transform=None, **kwargs): The extents of the path Bbox([[xmin, ymin], [xmax, ymax]]) """ from .transforms import Bbox - if transform is not None: self = transform.transform_path(self) if self.codes is None: @@ -690,7 +649,8 @@ def get_extents(self, transform=None, **kwargs): # Instead of iterating through each curve, consider # each line segment's end-points # (recall that STOP and CLOSEPOLY vertices are ignored) - xys = self.vertices[np.isin(self.codes, [Path.MOVETO, Path.LINETO])] + xys = self.vertices[np.isin(self.codes, + [Path.MOVETO, Path.LINETO])] else: xys = [] for curve, code in self.iter_bezier(**kwargs): @@ -723,8 +683,7 @@ def intersects_bbox(self, bbox, filled=True): The bounding box is always considered filled. """ return _path.path_intersects_rectangle( - self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled - ) + self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled) def interpolated(self, steps): """ @@ -747,16 +706,10 @@ def interpolated(self, steps): if self.codes is not None and self.MOVETO in self.codes[1:]: return self.make_compound_path( - *(p.interpolated(steps) for p in self._iter_connected_components()) - ) - - if ( - self.codes is not None - and self.CLOSEPOLY in self.codes - and not np.all( - self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0] - ) - ): + *(p.interpolated(steps) for p in self._iter_connected_components())) + + if self.codes is not None and self.CLOSEPOLY in self.codes and not np.all( + self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0]): vertices = self.vertices.copy() vertices[self.codes == self.CLOSEPOLY] = vertices[0] else: @@ -765,9 +718,8 @@ def interpolated(self, steps): vertices = simple_linear_interpolation(vertices, steps) codes = self.codes if codes is not None: - new_codes = np.full( - (len(codes) - 1) * steps + 1, Path.LINETO, dtype=self.code_type - ) + new_codes = np.full((len(codes) - 1) * steps + 1, Path.LINETO, + dtype=self.code_type) new_codes[0::steps] = codes else: new_codes = None @@ -818,8 +770,7 @@ def to_polygons(self, transform=None, width=0, height=0, closed_only=True): # Deal with the case where there are curves and/or multiple # subpaths (using extension code) return _path.convert_path_to_polygons( - self, transform, width, height, closed_only - ) + self, transform, width, height, closed_only) _unit_rectangle = None @@ -829,9 +780,8 @@ def unit_rectangle(cls): Return a `Path` instance of the unit rectangle from (0, 0) to (1, 1). """ if cls._unit_rectangle is None: - cls._unit_rectangle = cls( - [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], closed=True, readonly=True - ) + cls._unit_rectangle = cls([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]], + closed=True, readonly=True) return cls._unit_rectangle _unit_regular_polygons = WeakValueDictionary() @@ -848,12 +798,10 @@ def unit_regular_polygon(cls, numVertices): else: path = None if path is None: - theta = ( - (2 * np.pi / numVertices) * np.arange(numVertices + 1) - # This initial rotation is to make sure the polygon always - # "points-up". - + np.pi / 2 - ) + theta = ((2 * np.pi / numVertices) * np.arange(numVertices + 1) + # This initial rotation is to make sure the polygon always + # "points-up". + + np.pi / 2) verts = np.column_stack((np.cos(theta), np.sin(theta))) path = cls(verts, closed=True, readonly=True) if numVertices <= 16: @@ -874,7 +822,7 @@ def unit_regular_star(cls, numVertices, innerCircle=0.5): path = None if path is None: ns2 = numVertices * 2 - theta = 2 * np.pi / ns2 * np.arange(ns2 + 1) + theta = (2*np.pi/ns2 * np.arange(ns2 + 1)) # This initial rotation is to make sure the polygon always # "points-up" theta += np.pi / 2.0 @@ -904,11 +852,12 @@ def unit_circle(cls): For most cases, :func:`Path.circle` will be what you want. """ if cls._unit_circle is None: - cls._unit_circle = cls.circle(center=(0, 0), radius=1, readonly=True) + cls._unit_circle = cls.circle(center=(0, 0), radius=1, + readonly=True) return cls._unit_circle @classmethod - def circle(cls, center=(0.0, 0.0), radius=1.0, readonly=False): + def circle(cls, center=(0., 0.), radius=1., readonly=False): """ Return a `Path` representing a circle of a given radius and center. @@ -933,37 +882,42 @@ def circle(cls, center=(0.0, 0.0), radius=1.0, readonly=False): SQRTHALF = np.sqrt(0.5) MAGIC45 = SQRTHALF * MAGIC - vertices = np.array( - [ - [0.0, -1.0], - [MAGIC, -1.0], - [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], - [SQRTHALF, -SQRTHALF], - [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], - [1.0, -MAGIC], - [1.0, 0.0], - [1.0, MAGIC], - [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], - [SQRTHALF, SQRTHALF], - [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], - [MAGIC, 1.0], - [0.0, 1.0], - [-MAGIC, 1.0], - [-SQRTHALF + MAGIC45, SQRTHALF + MAGIC45], - [-SQRTHALF, SQRTHALF], - [-SQRTHALF - MAGIC45, SQRTHALF - MAGIC45], - [-1.0, MAGIC], - [-1.0, 0.0], - [-1.0, -MAGIC], - [-SQRTHALF - MAGIC45, -SQRTHALF + MAGIC45], - [-SQRTHALF, -SQRTHALF], - [-SQRTHALF + MAGIC45, -SQRTHALF - MAGIC45], - [-MAGIC, -1.0], - [0.0, -1.0], - [0.0, -1.0], - ], - dtype=float, - ) + vertices = np.array([[0.0, -1.0], + + [MAGIC, -1.0], + [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], + [SQRTHALF, -SQRTHALF], + + [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], + [1.0, -MAGIC], + [1.0, 0.0], + + [1.0, MAGIC], + [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], + [SQRTHALF, SQRTHALF], + + [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], + [MAGIC, 1.0], + [0.0, 1.0], + + [-MAGIC, 1.0], + [-SQRTHALF+MAGIC45, SQRTHALF+MAGIC45], + [-SQRTHALF, SQRTHALF], + + [-SQRTHALF-MAGIC45, SQRTHALF-MAGIC45], + [-1.0, MAGIC], + [-1.0, 0.0], + + [-1.0, -MAGIC], + [-SQRTHALF-MAGIC45, -SQRTHALF+MAGIC45], + [-SQRTHALF, -SQRTHALF], + + [-SQRTHALF+MAGIC45, -SQRTHALF-MAGIC45], + [-MAGIC, -1.0], + [0.0, -1.0], + + [0.0, -1.0]], + dtype=float) codes = [cls.CURVE4] * 26 codes[0] = cls.MOVETO @@ -985,24 +939,27 @@ def unit_circle_righthalf(cls): MAGIC45 = SQRTHALF * MAGIC vertices = np.array( - [ - [0.0, -1.0], - [MAGIC, -1.0], - [SQRTHALF - MAGIC45, -SQRTHALF - MAGIC45], - [SQRTHALF, -SQRTHALF], - [SQRTHALF + MAGIC45, -SQRTHALF + MAGIC45], - [1.0, -MAGIC], - [1.0, 0.0], - [1.0, MAGIC], - [SQRTHALF + MAGIC45, SQRTHALF - MAGIC45], - [SQRTHALF, SQRTHALF], - [SQRTHALF - MAGIC45, SQRTHALF + MAGIC45], - [MAGIC, 1.0], - [0.0, 1.0], - [0.0, -1.0], - ], - float, - ) + [[0.0, -1.0], + + [MAGIC, -1.0], + [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], + [SQRTHALF, -SQRTHALF], + + [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], + [1.0, -MAGIC], + [1.0, 0.0], + + [1.0, MAGIC], + [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], + [SQRTHALF, SQRTHALF], + + [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], + [MAGIC, 1.0], + [0.0, 1.0], + + [0.0, -1.0]], + + float) codes = np.full(14, cls.CURVE4, dtype=cls.code_type) codes[0] = cls.MOVETO @@ -1083,10 +1040,10 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False): vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot - vertices[vertex_offset + 1 : end : 3, 0] = xB - alpha * xB_dot - vertices[vertex_offset + 1 : end : 3, 1] = yB - alpha * yB_dot - vertices[vertex_offset + 2 : end : 3, 0] = xB - vertices[vertex_offset + 2 : end : 3, 1] = yB + vertices[vertex_offset+1:end:3, 0] = xB - alpha * xB_dot + vertices[vertex_offset+1:end:3, 1] = yB - alpha * yB_dot + vertices[vertex_offset+2:end:3, 0] = xB + vertices[vertex_offset+2:end:3, 1] = yB return cls(vertices, codes, readonly=True) @@ -1117,8 +1074,8 @@ def hatch(hatchpattern, density=6): number of lines per unit square. """ from matplotlib.hatch import get_path - - return get_path(hatchpattern, density) if hatchpattern is not None else None + return (get_path(hatchpattern, density) + if hatchpattern is not None else None) def clip_to_bbox(self, bbox, inside=True): """ @@ -1136,8 +1093,7 @@ def clip_to_bbox(self, bbox, inside=True): def get_path_collection_extents( - master_transform, paths, transforms, offsets, offset_transform -): + master_transform, paths, transforms, offsets, offset_transform): r""" Get bounding box of a `.PathCollection`\s internal objects. @@ -1167,12 +1123,11 @@ def get_path_collection_extents( - (C, α, O) """ from .transforms import Bbox - if len(paths) == 0: raise ValueError("No paths provided") if len(offsets) == 0: raise ValueError("No offsets provided") extents, minpos = _path.get_path_collection_extents( - master_transform, paths, np.atleast_3d(transforms), offsets, offset_transform - ) + master_transform, paths, np.atleast_3d(transforms), + offsets, offset_transform) return Bbox.from_extents(*extents, minpos=minpos)