diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 234e5a238436..cca56cb3c7b1 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -38,7 +38,7 @@ if T.TYPE_CHECKING: from collections.abc import Iterable - from .ft2font import CharacterCodeType, Glyph + from .ft2font import CharacterCodeType, Glyph, GlyphIndexType ParserElement.enable_packrat() @@ -87,7 +87,7 @@ class VectorParse(NamedTuple): width: float height: float depth: float - glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]] + glyphs: list[tuple[FT2Font, float, GlyphIndexType, float, float]] rects: list[tuple[float, float, float, float]] VectorParse.__module__ = "matplotlib.mathtext" @@ -132,7 +132,7 @@ def __init__(self, box: Box): def to_vector(self) -> VectorParse: w, h, d = map( np.ceil, [self.box.width, self.box.height, self.box.depth]) - gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + gs = [(info.font, info.fontsize, info.glyph_id, ox, h - oy + info.offset) for ox, oy, info in self.glyphs] rs = [(x1, h - y2, x2 - x1, y2 - y1) for x1, y1, x2, y2 in self.rects] @@ -214,7 +214,7 @@ class FontInfo(NamedTuple): fontsize: float postscript_name: str metrics: FontMetrics - num: CharacterCodeType + glyph_id: GlyphIndexType glyph: Glyph offset: float @@ -375,7 +375,8 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, dpi: float) -> FontInfo: font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char(num, flags=self.load_glyph_flags) + glyph_id = font.get_char_index(num) + glyph = font.load_glyph(glyph_id, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = (val / 64 for val in glyph.bbox) offset = self._get_offset(font, glyph, fontsize, dpi) @@ -397,7 +398,7 @@ def _get_info(self, fontname: str, font_class: str, sym: str, fontsize: float, fontsize=fontsize, postscript_name=font.postscript_name, metrics=metrics, - num=num, + glyph_id=glyph_id, glyph=glyph, offset=offset ) @@ -427,8 +428,7 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) font = info1.font - return font.get_kerning(font.get_char_index(info1.num), - font.get_char_index(info2.num), + return font.get_kerning(info1.glyph_id, info2.glyph_id, Kerning.DEFAULT) / 64 return super().get_kern(font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index 1a9b4e4c989c..22b52f943af6 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -14,7 +14,7 @@ class LayoutItem: ft_object: FT2Font char: str - glyph_idx: GlyphIndexType + glyph_index: GlyphIndexType x: float prev_kern: float diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a2a878d54156..21f1e82f5956 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -20,18 +20,18 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) -def get_glyphs_subset(fontfile, characters): +def get_glyphs_subset(fontfile, glyphs): """ - Subset a TTF font + Subset a TTF font. - Reads the named fontfile and restricts the font to the characters. + Reads the named fontfile and restricts the font to the glyphs. Parameters ---------- fontfile : str Path to the font file - characters : str - Continuous set of characters to include in subset + glyphs : set[int] + Set of glyph IDs to include in subset. Returns ------- @@ -39,8 +39,8 @@ def get_glyphs_subset(fontfile, characters): An open font object representing the subset, which needs to be closed by the caller. """ - - options = subset.Options(glyph_names=True, recommended_glyphs=True) + options = subset.Options(glyph_names=True, recommended_glyphs=True, + retain_gids=True) # Prevent subsetting extra tables. options.drop_tables += [ @@ -71,7 +71,7 @@ def get_glyphs_subset(fontfile, characters): font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) - subsetter.populate(text=characters) + subsetter.populate(gids=glyphs) subsetter.subset(font) return font @@ -97,10 +97,10 @@ def font_as_file(font): class CharacterTracker: """ - Helper for font subsetting by the pdf and ps backends. + Helper for font subsetting by the PDF and PS backends. - Maintains a mapping of font paths to the set of character codepoints that - are being used from that font. + Maintains a mapping of font paths to the set of glyphs that are being used from that + font. """ def __init__(self): @@ -110,10 +110,11 @@ def track(self, font, s): """Record that string *s* is being typeset using font *font*.""" char_to_font = font._get_fontmap(s) for _c, _f in char_to_font.items(): - self.used.setdefault(_f.fname, set()).add(ord(_c)) + glyph_index = _f.get_char_index(ord(_c)) + self.used.setdefault(_f.fname, set()).add(glyph_index) def track_glyph(self, font, glyph): - """Record that codepoint *glyph* is being typeset using font *font*.""" + """Record that glyph index *glyph* is being typeset using font *font*.""" self.used.setdefault(font.fname, set()).add(glyph) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 7409cd35b394..e20ec3fc2313 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -8,6 +8,7 @@ import functools import gzip +import itertools import math import numpy as np @@ -248,13 +249,12 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if angle: ctx.rotate(np.deg2rad(-angle)) - for font, fontsize, idx, ox, oy in glyphs: + for (font, fontsize), font_glyphs in itertools.groupby( + glyphs, key=lambda info: (info[0], info[1])): ctx.new_path() - ctx.move_to(ox, -oy) - ctx.select_font_face( - *_cairo_font_args_from_font_prop(ttfFontProperty(font))) + ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_text(chr(idx)) + ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) for ox, oy, w, h in rects: ctx.new_path() diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index ff351e301176..1ee46824a7be 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -960,9 +960,9 @@ def writeFonts(self): else: # a normal TrueType font _log.debug('Writing TrueType font.') - chars = self._character_tracker.used.get(filename) - if chars: - fonts[Fx] = self.embedTTF(filename, chars) + glyphs = self._character_tracker.used.get(filename) + if glyphs: + fonts[Fx] = self.embedTTF(filename, glyphs) self.writeObject(self.fontObject, fonts) def _write_afm_font(self, filename): @@ -1136,9 +1136,8 @@ def _get_xobject_glyph_name(self, filename, glyph_name): end end""" - def embedTTF(self, filename, characters): + def embedTTF(self, filename, glyphs): """Embed the TTF font from the named file into the document.""" - font = get_font(filename) fonttype = mpl.rcParams['pdf.fonttype'] @@ -1153,7 +1152,7 @@ def cvt(length, upe=font.units_per_EM, nearest=True): else: return math.ceil(value) - def embedTTFType3(font, characters, descriptor): + def embedTTFType3(font, glyphs, descriptor): """The Type 3-specific part of embedding a Truetype font""" widthsObject = self.reserveObject('font widths') fontdescObject = self.reserveObject('font descriptor') @@ -1200,15 +1199,13 @@ def get_char_width(charcode): # Make the "Differences" array, sort the ccodes < 255 from # the multi-byte ccodes, and build the whole set of glyph ids # that we need from this font. - glyph_ids = [] differences = [] multi_byte_chars = set() - for c in characters: - ccode = c - gind = font.get_char_index(ccode) - glyph_ids.append(gind) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: glyph_name = font.get_glyph_name(gind) - if ccode <= 255: + ccode = charmap.get(gind) + if ccode is not None and ccode <= 255: differences.append((ccode, glyph_name)) else: multi_byte_chars.add(glyph_name) @@ -1222,7 +1219,7 @@ def get_char_width(charcode): last_c = c # Make the charprocs array. - rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + rawcharprocs = _get_pdf_charprocs(filename, glyphs) charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] @@ -1259,7 +1256,7 @@ def get_char_width(charcode): return fontdictObject - def embedTTFType42(font, characters, descriptor): + def embedTTFType42(font, glyphs, descriptor): """The Type 42-specific part of embedding a Truetype font""" fontdescObject = self.reserveObject('font descriptor') cidFontDictObject = self.reserveObject('CID font dictionary') @@ -1269,9 +1266,8 @@ def embedTTFType42(font, characters, descriptor): 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: + _log.debug("SUBSET %s characters: %s", filename, glyphs) + with _backend_pdf_ps.get_glyphs_subset(filename, glyphs) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( "SUBSET %s %d -> %d", filename, @@ -1319,11 +1315,11 @@ def embedTTFType42(font, characters, descriptor): 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) + charmap = {gind: ccode for ccode, gind in font.get_charmap().items()} + for gind in glyphs: + glyph = font.load_glyph(gind, + flags=LoadFlags.NO_SCALE | LoadFlags.NO_HINTING) + ccode = charmap[gind] widths.append((ccode, cvt(glyph.horiAdvance))) if ccode < 65536: cid_to_gid_map[ccode] = chr(gind) @@ -1361,11 +1357,10 @@ def embedTTFType42(font, characters, descriptor): (len(unicode_groups), b"\n".join(unicode_bfrange))) # Add XObjects for unsupported chars - glyph_ids = [] - for ccode in characters: - if not _font_supports_glyph(fonttype, ccode): - gind = full_font.get_char_index(ccode) - glyph_ids.append(gind) + glyph_ids = [ + gind for gind in glyphs + if not _font_supports_glyph(fonttype, charmap[gind]) + ] bbox = [cvt(x, nearest=False) for x in full_font.bbox] rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) @@ -1450,9 +1445,9 @@ def embedTTFType42(font, characters, descriptor): } if fonttype == 3: - return embedTTFType3(font, characters, descriptor) + return embedTTFType3(font, glyphs, descriptor) elif fonttype == 42: - return embedTTFType42(font, characters, descriptor) + return embedTTFType42(font, glyphs, descriptor) def alphaState(self, alpha): """Return name of an ExtGState that sets alpha to the given value.""" @@ -2215,14 +2210,19 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): oldx, oldy = 0, 0 unsupported_chars = [] + font_charmaps = {} self.file.output(Op.begin_text) - for font, fontsize, num, ox, oy in glyphs: - self.file._character_tracker.track_glyph(font, num) + for font, fontsize, glyph_index, ox, oy in glyphs: + self.file._character_tracker.track_glyph(font, glyph_index) fontname = font.fname - if not _font_supports_glyph(fonttype, num): + if font not in font_charmaps: + font_charmaps[font] = {gind: ccode + for ccode, gind in font.get_charmap().items()} + ccode = font_charmaps[font].get(glyph_index) + if ccode is None or not _font_supports_glyph(fonttype, ccode): # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in # Type 42) must be emitted separately (below). - unsupported_chars.append((font, fontsize, ox, oy, num)) + unsupported_chars.append((font, fontsize, ox, oy, glyph_index)) else: self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy @@ -2230,13 +2230,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.file.output(self.file.fontName(fontname), fontsize, Op.selectfont) prev_font = fontname, fontsize - self.file.output(self.encode_string(chr(num), fonttype), + self.file.output(self.encode_string(chr(ccode), 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) + for font, fontsize, ox, oy, glyph_index in unsupported_chars: + self._draw_xobject_glyph(font, fontsize, glyph_index, ox, oy) # Draw any horizontal lines in the math layout for ox, oy, width, height in rects: @@ -2274,7 +2273,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): seq += [['font', pdfname, dvifont.size]] oldfont = dvifont seq += [['text', x1, y1, [bytes([glyph])], x1+width]] - self.file._character_tracker.track(dvifont, chr(glyph)) + self.file._character_tracker.track_glyph(dvifont, glyph) # Find consecutive text strings with constant y coordinate and # combine into a sequence of strings and kerns, or just one @@ -2399,7 +2398,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): singlebyte_chunks[-1][2].append(item.char) prev_was_multibyte = False else: - multibyte_glyphs.append((item.ft_object, item.x, item.glyph_idx)) + multibyte_glyphs.append((item.ft_object, item.x, item.glyph_index)) prev_was_multibyte = True # Do the rotation and global translation as a single matrix # concatenation up front @@ -2409,7 +2408,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): -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) prev_start_x = 0 for ft_object, start_x, kerns_or_chars in singlebyte_chunks: diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 368564a1518d..f55d093a67bf 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -88,16 +88,16 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, chars): +def _font_to_ps_type3(font_path, glyph_indices): """ - Subset *chars* from the font at *font_path* into a Type 3 font. + Subset *glyphs_indices* from the font at *font_path* into a Type 3 font. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. Returns ------- @@ -106,7 +106,6 @@ def _font_to_ps_type3(font_path, chars): verbatim into a PostScript file. """ font = get_font(font_path, hinting_factor=1) - glyph_ids = [font.get_char_index(c) for c in chars] preamble = """\ %!PS-Adobe-3.0 Resource-Font @@ -123,9 +122,9 @@ def _font_to_ps_type3(font_path, chars): """.format(font_name=font.postscript_name, inv_units_per_em=1 / font.units_per_EM, bbox=" ".join(map(str, font.bbox)), - encoding=" ".join(f"/{font.get_glyph_name(glyph_id)}" - for glyph_id in glyph_ids), - num_glyphs=len(glyph_ids) + 1) + encoding=" ".join(f"/{font.get_glyph_name(glyph_index)}" + for glyph_index in glyph_indices), + num_glyphs=len(glyph_indices) + 1) postamble = """ end readonly def @@ -146,12 +145,12 @@ def _font_to_ps_type3(font_path, chars): """ entries = [] - for glyph_id in glyph_ids: - g = font.load_glyph(glyph_id, LoadFlags.NO_SCALE) + for glyph_index in glyph_indices: + g = font.load_glyph(glyph_index, LoadFlags.NO_SCALE) v, c = font.get_path() entries.append( "/%(name)s{%(bbox)s sc\n" % { - "name": font.get_glyph_name(glyph_id), + "name": font.get_glyph_name(glyph_index), "bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])), } + _path.convert_to_string( @@ -169,21 +168,20 @@ def _font_to_ps_type3(font_path, chars): return preamble + "\n".join(entries) + postamble -def _font_to_ps_type42(font_path, chars, fh): +def _font_to_ps_type42(font_path, glyph_indices, fh): """ - Subset *chars* from the font at *font_path* into a Type 42 font at *fh*. + Subset *glyph_indices* from the font at *font_path* into a Type 42 font at *fh*. Parameters ---------- font_path : path-like Path to the font to be subsetted. - chars : str - The characters to include in the subsetted font. + glyph_indices : set[int] + The glyphs to include in the subsetted font. fh : file-like Where to write the font. """ - subset_str = ''.join(chr(c) for c in chars) - _log.debug("SUBSET %s characters: %s", font_path, subset_str) + _log.debug("SUBSET %s characters: %s", font_path, glyph_indices) try: kw = {} # fix this once we support loading more fonts from a collection @@ -191,7 +189,7 @@ def _font_to_ps_type42(font_path, chars, fh): if font_path.endswith('.ttc'): kw['fontNumber'] = 0 with (fontTools.ttLib.TTFont(font_path, **kw) as font, - _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset): + _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( "SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, @@ -775,8 +773,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) - ps_name = (font.postscript_name.encode("ascii", "replace") - .decode("ascii")) + ps_name = font.postscript_name.encode("ascii", "replace").decode("ascii") scale = 0.001 * prop.get_size_in_points() thisx = 0 last_name = '' # kerns returns 0 for ''. @@ -799,7 +796,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): for item in _text_helpers.layout(s, font): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) - glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) + glyph_name = item.ft_object.get_glyph_name(item.glyph_index) stream.append((ps_name, item.x, glyph_name)) self.set_color(*gc.get_rgb()) @@ -828,13 +825,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): f"{x:g} {y:g} translate\n" f"{angle:g} rotate\n") lastfont = None - for font, fontsize, num, ox, oy in glyphs: - self._character_tracker.track_glyph(font, num) + for font, fontsize, glyph_index, ox, oy in glyphs: + self._character_tracker.track_glyph(font, glyph_index) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - glyph_name = font.get_glyph_name(font.get_char_index(num)) + glyph_name = font.get_glyph_name(glyph_index) self._pswriter.write( f"{ox:g} {oy:g} moveto\n" f"/{glyph_name} glyphshow\n") @@ -1072,19 +1069,18 @@ def print_figure_impl(fh): print("mpldict begin", file=fh) print("\n".join(_psDefs), file=fh) if not mpl.rcParams['ps.useafm']: - for font_path, chars \ - in ps_renderer._character_tracker.used.items(): - if not chars: + for font_path, glyphs in ps_renderer._character_tracker.used.items(): + if not glyphs: continue fonttype = mpl.rcParams['ps.fonttype'] # Can't use more than 255 chars from a single Type 3 font. - if len(chars) > 255: + if len(glyphs) > 255: fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, chars)) + fh.write(_font_to_ps_type3(font_path, glyphs)) else: # Type 42 only. - _font_to_ps_type42(font_path, chars, fh) + _font_to_ps_type42(font_path, glyphs, fh) print("end", file=fh) print("%%EndProlog", file=fh) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..d85ba4893958 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1023,19 +1023,19 @@ def _update_glyph_map_defs(self, glyph_map_new): writer = self.writer if glyph_map_new: writer.start('defs') - for char_id, (vertices, codes) in glyph_map_new.items(): - char_id = self._adjust_char_id(char_id) + for glyph_id, (vertices, codes) in glyph_map_new.items(): + glyph_id = self._adjust_glyph_id(glyph_id) # x64 to go back to FreeType's internal (integral) units. path_data = self._convert_path( Path(vertices * 64, codes), simplify=False) writer.element( - 'path', id=char_id, d=path_data, + 'path', id=glyph_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): - return char_id.replace("%20", "_") + def _adjust_glyph_id(self, glyph_id): + return glyph_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # docstring inherited @@ -1067,9 +1067,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) - _glyphs = text2path.get_glyphs_with_font( + glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( 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: @@ -1091,15 +1090,15 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): 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) + for glyph_id, xposition, yposition, scale in glyph_info: + glyph_id = self._adjust_glyph_id(glyph_id) writer.element( 'use', transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), - attrib={'xlink:href': f'#{char_id}'}) + attrib={'xlink:href': f'#{glyph_id}'}) for verts, codes in rects: path = Path(verts, codes) @@ -1223,7 +1222,12 @@ def _get_all_quoted_names(prop): # Sort the characters by font, and output one tspan for each. spans = {} - for font, fontsize, thetext, new_x, new_y in glyphs: + font_charmaps = {} + for font, fontsize, glyph_index, new_x, new_y in glyphs: + if font not in font_charmaps: + font_charmaps[font] = { + gind: ccode for ccode, gind in font.get_charmap().items()} + ccode = font_charmaps[font].get(glyph_index) entry = fm.ttfFontProperty(font) font_style = {} # Separate font style in its separate attributes @@ -1238,9 +1242,9 @@ def _get_all_quoted_names(prop): 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 - spans.setdefault(style, []).append((new_x, -new_y, thetext)) + if ccode == 32: + ccode = 0xa0 # non-breaking space + spans.setdefault(style, []).append((new_x, -new_y, ccode)) for style, chars in spans.items(): chars.sort() # Sort by increasing x position diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index f126fb543e78..fdf0ebbfc871 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -361,13 +361,13 @@ def test_glyphs_subset(): # non-subsetted FT2Font nosubfont = FT2Font(fpath) nosubfont.set_text(chars) + nosubcmap = nosubfont.get_charmap() # subsetted FT2Font - with get_glyphs_subset(fpath, chars) as subset: + glyph_ids = {nosubcmap[ord(c)] for c in chars} + with get_glyphs_subset(fpath, glyph_ids) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) - - nosubcmap = nosubfont.get_charmap() subcmap = subfont.get_charmap() # all unique chars must be available in subsetted font diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 2c64b7c24b3e..e865dbbe92da 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -216,7 +216,7 @@ def test_unicode_won(): tree = xml.etree.ElementTree.fromstring(buf) ns = 'http://www.w3.org/2000/svg' - won_id = 'SFSS1728-8e' + won_id = 'SFSS1728-232' assert len(tree.findall(f'.//{{{ns}}}path[@d][@id="{won_id}"]')) == 1 assert f'#{won_id}' in tree.find(f'.//{{{ns}}}use').attrib.values() diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..02e4cdf27b80 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -39,11 +39,9 @@ def _get_font(self, prop): def _get_hinting_flag(self): return LoadFlags.NO_HINTING - def _get_char_id(self, font, ccode): - """ - Return a unique id for the given font and character-code set. - """ - return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}") + def _get_glyph_id(self, font, glyph): + """Return a unique id for the given font and glyph index.""" + return urllib.parse.quote(f"{font.postscript_name}-{glyph:x}") def get_text_width_height_descent(self, s, prop, ismath): fontsize = prop.get_size_in_points() @@ -146,11 +144,11 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_ids = [] for item in _text_helpers.layout(s, font): - char_id = self._get_char_id(item.ft_object, ord(item.char)) - glyph_ids.append(char_id) + glyph_id = self._get_glyph_id(item.ft_object, item.glyph_index) + glyph_ids.append(glyph_id) xpositions.append(item.x) - if char_id not in glyph_map: - glyph_map_new[char_id] = item.ft_object.get_path() + if glyph_id not in glyph_map: + glyph_map_new[glyph_id] = item.ft_object.get_path() ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) @@ -185,17 +183,17 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, glyph_ids = [] sizes = [] - for font, fontsize, ccode, ox, oy in glyphs: - char_id = self._get_char_id(font, ccode) - if char_id not in glyph_map: + for font, fontsize, glyph_index, ox, oy in glyphs: + glyph_id = self._get_glyph_id(font, glyph_index) + if glyph_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - font.load_char(ccode, flags=LoadFlags.NO_HINTING) - glyph_map_new[char_id] = font.get_path() + font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) + glyph_map_new[glyph_id] = font.get_path() xpositions.append(ox) ypositions.append(oy) - glyph_ids.append(char_id) + glyph_ids.append(glyph_id) size = fontsize / self.FONT_SCALE sizes.append(size) @@ -232,17 +230,16 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # Gather font information and do some setup for combining # characters into strings. - t1_encodings = {} for text in page.text: font = get_font(text.font_path) - char_id = self._get_char_id(font, text.glyph) - if char_id not in glyph_map: + glyph_id = self._get_glyph_id(font, text.index) + if glyph_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) font.load_glyph(text.index, flags=LoadFlags.TARGET_LIGHT) - glyph_map_new[char_id] = font.get_path() + glyph_map_new[glyph_id] = font.get_path() - glyph_ids.append(char_id) + glyph_ids.append(glyph_id) xpositions.append(text.x) ypositions.append(text.y) sizes.append(text.font_size / self.FONT_SCALE)