diff --git a/lib/matplotlib/_text_layout.py b/lib/matplotlib/_text_layout.py new file mode 100644 index 000000000000..a06edaf63ab7 --- /dev/null +++ b/lib/matplotlib/_text_layout.py @@ -0,0 +1,40 @@ +""" +Text layouting utilities. +""" + +from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING + + +def layout(string, font, *, x0=0, kern_mode=KERNING_DEFAULT): + """ + Render *string* with *font*. For each character in *string*, yield a + (character-index, x-position) pair. When such a pair is yielded, the + font's glyph is set to the corresponding character. + + Parameters + ---------- + string : str + The string to be rendered. + font : FT2Font + The font. + x0 : float + The initial x-value + kern_mode : int + A FreeType kerning mode. + + Yields + ------ + character_index : int + x_position : float + """ + x = x0 + last_char_idx = None + for char in string: + char_idx = font.get_char_index(ord(char)) + kern = (font.get_kerning(last_char_idx, char_idx, kern_mode) + if last_char_idx is not None else 0) / 64 + x += kern + glyph = font.load_glyph(char_idx, flags=LOAD_NO_HINTING) + yield char_idx, x + x += glyph.linearHoriAdvance / 65536 + last_char_idx = char_idx diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c26fd70002da..e3a5a8f5d18d 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -21,7 +21,7 @@ import numpy as np -from matplotlib import cbook, __version__, rcParams +from matplotlib import _text_layout, cbook, __version__, rcParams from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, @@ -2176,52 +2176,46 @@ def draw_text_woven(chunks): -math.sin(a), math.cos(a), x, y, Op.concat_matrix) - # Output all the 1-byte characters in a BT/ET group, then - # output all the 2-byte characters. - for mode in (1, 2): - newx = oldx = 0 - # Output a 1-byte character chunk - if mode == 1: - self.file.output(Op.begin_text, - self.file.fontName(prop), - fontsize, - Op.selectfont) - - for chunk_type, chunk in chunks: - if mode == 1 and chunk_type == 1: - self._setup_textpos(newx, 0, 0, oldx, 0, 0) - self.file.output(self.encode_string(chunk, fonttype), - Op.show) - oldx = newx - - lastgind = None - for c in chunk: - ccode = ord(c) - gind = font.get_char_index(ccode) - if mode == 2 and chunk_type == 2: - glyph_name = font.get_glyph_name(gind) - self.file.output(Op.gsave) - self.file.output(0.001 * fontsize, 0, - 0, 0.001 * fontsize, - newx, 0, Op.concat_matrix) - name = self.file._get_xobject_symbol_name( - font.fname, glyph_name) - self.file.output(Name(name), Op.use_xobject) - self.file.output(Op.grestore) - - # Move the pointer based on the character width - # and kerning - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) - if lastgind is not None: - kern = font.get_kerning( - lastgind, gind, KERNING_UNFITTED) - else: - kern = 0 - lastgind = gind - newx += kern / 64 + glyph.linearHoriAdvance / 65536 - - if mode == 1: - self.file.output(Op.end_text) + # Output all the 1-byte characters in a BT/ET group. + self.file.output(Op.begin_text, + self.file.fontName(prop), + fontsize, + Op.selectfont) + newx = oldx = 0 + for chunk_type, chunk in chunks: + if chunk_type == 1: + self._setup_textpos(newx, 0, 0, oldx, 0, 0) + self.file.output(self.encode_string(chunk, fonttype), + Op.show) + oldx = newx + # Update newx to include the advance from this chunk, + # regardless of its mode... + for char_idx, char_x in _text_layout.layout( + chunk, font, x0=newx, kern_mode=KERNING_UNFITTED): + pass + newx = char_x + ( # ... including the last character's advance + font.load_glyph(char_idx, flags=LOAD_NO_HINTING) + .linearHoriAdvance / 65536) + self.file.output(Op.end_text) + + # Then output all the 2-byte characters. + newx = 0 + for chunk_type, chunk in chunks: + for char_idx, char_x in _text_layout.layout( + chunk, font, x0=newx, kern_mode=KERNING_UNFITTED): + if chunk_type == 2: + glyph_name = font.get_glyph_name(char_idx) + self.file.output(Op.gsave) + self.file.output(0.001 * fontsize, 0, + 0, 0.001 * fontsize, + char_x, 0, Op.concat_matrix) + name = self.file._get_xobject_symbol_name( + font.fname, glyph_name) + self.file.output(Name(name), Op.use_xobject) + self.file.output(Op.grestore) + newx = char_x + ( # ... including the last character's advance + font.load_glyph(char_idx, flags=LOAD_NO_HINTING) + .linearHoriAdvance / 65536) self.file.output(Op.grestore) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 9125ef4cdc62..727bebbd033e 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -20,13 +20,14 @@ import matplotlib as mpl from matplotlib import ( cbook, _path, __version__, rcParams, checkdep_ghostscript) +from matplotlib import _text_layout from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) from matplotlib.cbook import (get_realpath_and_stat, is_writable_file_like, file_requires_unicode) from matplotlib.font_manager import is_opentype_cff_font, get_font -from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING +from matplotlib.ft2font import LOAD_NO_HINTING from matplotlib.ttconv import convert_ttf_to_ps from matplotlib.mathtext import MathTextParser from matplotlib._mathtext_data import uni2type1 @@ -623,27 +624,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): .encode('ascii', 'replace').decode('ascii')) self.set_font(ps_name, prop.get_size_in_points()) - lastgind = None - lines = [] - thisx = 0 - thisy = 0 - for c in s: - ccode = ord(c) - gind = font.get_char_index(ccode) - name = font.get_glyph_name(gind) - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) - - if lastgind is not None: - kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) - else: - kern = 0 - lastgind = gind - thisx += kern / 64 - - lines.append('%f %f m /%s glyphshow' % (thisx, thisy, name)) - thisx += glyph.linearHoriAdvance / 65536 - - thetext = '\n'.join(lines) + thetext = '\n'.join( + '%f 0 m /%s glyphshow' % (x, font.get_glyph_name(char_idx)) + for char_idx, x in _text_layout.layout(s, font)) self._pswriter.write(f"""\ gsave {x:f} {y:f} translate diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 1f652940c261..5c96dde4102c 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -5,10 +5,9 @@ import numpy as np -from matplotlib import cbook, dviread, font_manager, rcParams +from matplotlib import _text_layout, cbook, dviread, font_manager, rcParams from matplotlib.font_manager import FontProperties, get_font -from matplotlib.ft2font import ( - KERNING_DEFAULT, LOAD_NO_HINTING, LOAD_TARGET_LIGHT) +from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Affine2D @@ -162,14 +161,6 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, Convert string *s* to vertices and codes using the provided ttf font. """ - # Mostly copied from backend_svg.py. - - lastgind = None - - currx = 0 - xpositions = [] - glyph_ids = [] - if glyph_map is None: glyph_map = OrderedDict() @@ -178,33 +169,15 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, else: glyph_map_new = glyph_map - # I'm not sure if I get kernings right. Needs to be verified. -JJL - - for c in s: - ccode = ord(c) - gind = font.get_char_index(ccode) - - if lastgind is not None: - kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT) - else: - kern = 0 - - glyph = font.load_char(ccode, flags=LOAD_NO_HINTING) - horiz_advance = glyph.linearHoriAdvance / 65536 - - char_id = self._get_char_id(font, ccode) + xpositions = [] + glyph_ids = [] + for char, (_, x) in zip(s, _text_layout.layout(s, font)): + char_id = self._get_char_id(font, ord(char)) + glyph_ids.append(char_id) + xpositions.append(x) if char_id not in glyph_map: glyph_map_new[char_id] = font.get_path() - currx += kern / 64 - - xpositions.append(currx) - glyph_ids.append(char_id) - - currx += horiz_advance - - lastgind = gind - ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions)