Skip to content
Prev Previous commit
Next Next commit
ENH: implement font fallback for PDF
  • Loading branch information
aitikgupta authored and tacaswell committed Aug 9, 2022
commit c5fd8804204ee715ee008c35f96d6e95f8dfcc29
14 changes: 10 additions & 4 deletions lib/matplotlib/_text_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


LayoutItem = dataclasses.make_dataclass(
"LayoutItem", ["char", "glyph_idx", "x", "prev_kern"])
"LayoutItem", ["ft_object", "char", "glyph_idx", "x", "prev_kern"])


def warn_on_missing_glyph(codepoint):
Expand Down Expand Up @@ -57,12 +57,18 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
"""
x = 0
prev_glyph_idx = None
char_to_font = font._get_fontmap(string)
base_font = font
for char in string:
# This has done the fallback logic
font = char_to_font.get(char, base_font)
glyph_idx = font.get_char_index(ord(char))
kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
if prev_glyph_idx is not None else 0.)
kern = (
base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
if prev_glyph_idx is not None else 0.
)
x += kern
glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
yield LayoutItem(char, glyph_idx, x, kern)
yield LayoutItem(font, char, glyph_idx, x, kern)
x += glyph.linearHoriAdvance / 65536
prev_glyph_idx = glyph_idx
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/_backend_pdf_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ def _get_font_afm(self, prop):
return _cached_get_afm_from_fname(fname)

def _get_font_ttf(self, prop):
fname = font_manager.findfont(prop)
font = font_manager.get_font(fname)
fnames = font_manager.fontManager._find_fonts_by_props(prop)
font = font_manager.get_font(fnames)
font.clear()
font.set_size(prop.get_size_in_points(), 72)
return font
70 changes: 44 additions & 26 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
RendererBase)
from matplotlib.backends.backend_mixed import MixedModeRenderer
from matplotlib.figure import Figure
from matplotlib.font_manager import findfont, get_font
from matplotlib.font_manager import (
findfont, get_font, fontManager as _fontManager
)
from matplotlib._afm import AFM
from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE,
LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font)
Expand Down Expand Up @@ -925,20 +927,28 @@ def fontName(self, fontprop):
"""

if isinstance(fontprop, str):
filename = fontprop
filenames = [fontprop]
elif mpl.rcParams['pdf.use14corefonts']:
filename = findfont(
fontprop, fontext='afm', directory=RendererPdf._afm_font_dir)
filenames = _fontManager._find_fonts_by_props(
fontprop, fontext='afm', directory=RendererPdf._afm_font_dir
)
else:
filename = findfont(fontprop)

Fx = self.fontNames.get(filename)
if Fx is None:
Fx = next(self._internal_font_seq)
self.fontNames[filename] = Fx
_log.debug('Assigning font %s = %r', Fx, filename)

return Fx
filenames = _fontManager._find_fonts_by_props(fontprop)
first_Fx = None
for fname in filenames:
Fx = self.fontNames.get(fname)
if not first_Fx:
first_Fx = Fx
if Fx is None:
Fx = next(self._internal_font_seq)
self.fontNames[fname] = Fx
_log.debug('Assigning font %s = %r', Fx, fname)
if not first_Fx:
first_Fx = Fx

# find_fontsprop's first value always adheres to
# findfont's value, so technically no behaviour change
return first_Fx

def dviFontName(self, dvifont):
"""
Expand Down Expand Up @@ -1204,7 +1214,6 @@ def get_char_width(charcode):
width = font.load_char(
s, flags=LOAD_NO_SCALE | LOAD_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
Expand Down Expand Up @@ -2389,22 +2398,27 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
# the regular text show command (TJ) with appropriate kerning between
# chunks, whereas multibyte characters use the XObject command (Do).
else:
# List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns.
# List of (ft_object, start_x, [prev_kern, char, char, ...]),
# w/o zero kerns.
singlebyte_chunks = []
# List of (start_x, glyph_index).
# List of (ft_object, start_x, glyph_index).
multibyte_glyphs = []
prev_was_multibyte = True
prev_font = font
for item in _text_helpers.layout(
s, font, kern_mode=KERNING_UNFITTED):
if _font_supports_glyph(fonttype, ord(item.char)):
if prev_was_multibyte:
singlebyte_chunks.append((item.x, []))
if prev_was_multibyte or item.ft_object != prev_font:
singlebyte_chunks.append((item.ft_object, item.x, []))
prev_font = item.ft_object
if item.prev_kern:
singlebyte_chunks[-1][1].append(item.prev_kern)
singlebyte_chunks[-1][1].append(item.char)
singlebyte_chunks[-1][2].append(item.prev_kern)
Comment thread
QuLogic marked this conversation as resolved.
singlebyte_chunks[-1][2].append(item.char)
prev_was_multibyte = False
else:
multibyte_glyphs.append((item.x, item.glyph_idx))
multibyte_glyphs.append(
(item.ft_object, item.x, item.glyph_idx)
)
prev_was_multibyte = True
# Do the rotation and global translation as a single matrix
# concatenation up front
Expand All @@ -2414,10 +2428,12 @@ 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,
self.file.fontName(prop), fontsize, Op.selectfont)

self.file.output(Op.begin_text)
prev_start_x = 0
for start_x, kerns_or_chars in singlebyte_chunks:
for ft_object, start_x, kerns_or_chars in singlebyte_chunks:
ft_name = self.file.fontName(ft_object.fname)
self.file.output(ft_name, fontsize, Op.selectfont)
self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
self.file.output(
# See pdf spec "Text space details" for the 1000/fontsize
Expand All @@ -2429,8 +2445,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
prev_start_x = start_x
self.file.output(Op.end_text)
# Then emit all the multibyte characters, one at a time.
for start_x, glyph_idx in multibyte_glyphs:
self._draw_xobject_glyph(font, fontsize, glyph_idx, start_x, 0)
for ft_object, start_x, glyph_idx in multibyte_glyphs:
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):
Expand Down
Binary file not shown.
Binary file not shown.
30 changes: 29 additions & 1 deletion lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import pytest

import matplotlib as mpl
from matplotlib import pyplot as plt, rcParams
from matplotlib import (
pyplot as plt, rcParams, font_manager as fm
)
from matplotlib.cbook import _get_data_path
from matplotlib.ft2font import FT2Font
from matplotlib.font_manager import findfont, FontProperties
Expand Down Expand Up @@ -383,3 +385,29 @@ def test_glyphs_subset():

# since both objects are assigned same characters
assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs()


@image_comparison(["multi_font_type3.pdf"])
def test_multi_font_type3():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")

plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('pdf', fonttype=3)

fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")


@image_comparison(["multi_font_type42.pdf"])
def test_multi_font_type42():
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")

plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
plt.rc('pdf', fonttype=42)

fig = plt.figure()
fig.text(0.15, 0.475, "There are 几个汉字 in between!")