8000 ENH: implement font fallback for PDF · matplotlib/matplotlib@c5fd880 · GitHub
[go: up one dir, main page]

Skip to content

Commit c5fd880

Browse files
aitikguptatacaswell
authored andcommitted
ENH: implement font fallback for PDF
1 parent 7fcde53 commit c5fd880

File tree

6 files changed

+85
-33
lines changed
  • tests
  • 6 files changed

    +85
    -33
    lines changed

    lib/matplotlib/_text_helpers.py

    Lines changed: 10 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -9,7 +9,7 @@
    99

    1010

    1111
    LayoutItem = dataclasses.make_dataclass(
    12-
    "LayoutItem", ["char", "glyph_idx", "x", "prev_kern"])
    12+
    "LayoutItem", ["ft_object", "char", "glyph_idx", "x", "prev_kern"])
    1313

    1414

    1515
    def warn_on_missing_glyph(codepoint):
    @@ -57,12 +57,18 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
    5757
    """
    5858
    x = 0
    5959
    prev_glyph_idx = None
    60+
    char_to_font = font._get_fontmap(string)
    61+
    base_font = font
    6062
    for char in string:
    63+
    # This has done the fallback logic
    64+
    font = char_to_font.get(char, base_font)
    6165
    glyph_idx = font.get_char_index(ord(char))
    62-
    kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
    63-
    if prev_glyph_idx is not None else 0.)
    66+
    kern = (
    67+
    base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
    68+
    if prev_glyph_idx is not None else 0.
    69+
    )
    6470
    x += kern
    6571
    glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING)
    66-
    yield LayoutItem(char, glyph_idx, x, kern)
    72+
    yield LayoutItem(font, char, glyph_idx, x, kern)
    6773
    x += glyph.linearHoriAdvance / 65536
    6874
    prev_glyph_idx = glyph_idx

    lib/matplotlib/backends/_backend_pdf_ps.py

    Lines changed: 2 additions & 2 deletions
    Original file line numberDiff line numberDiff line change
    @@ -137,8 +137,8 @@ def _get_font_afm(self, prop):
    137137
    return _cached_get_afm_from_fname(fname)
    138138

    139139
    def _get_font_ttf(self, prop):
    140-
    fname = font_manager.findfont(prop)
    141-
    font = font_manager.get_font(fname)
    140+
    fnames = font_manager.fontManager._find_fonts_by_props(prop)
    141+
    font = font_manager.get_font(fnames)
    142142
    font.clear()
    143143
    font.set_size(prop.get_size_in_points(), 72)
    144144
    return font

    lib/matplotlib/backends/backend_pdf.py

    Lines changed: 44 additions & 26 deletions
    Original file line numberDiff line numberDiff line change
    @@ -32,7 +32,9 @@
    3232
    RendererBase)
    3333
    from matplotlib.backends.backend_mixed import MixedModeRenderer
    3434
    from matplotlib.figure import Figure
    35-
    from matplotlib.font_manager import findfont, get_font
    35+
    from matplotlib.font_manager import (
    36+
    findfont, get_font, fontManager as _fontManager
    37+
    )
    3638
    from matplotlib._afm import AFM
    3739
    from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE,
    3840
    LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font)
    @@ -925,20 +927,28 @@ def fontName(self, fontprop):
    925927
    """
    926928

    927929
    if isinstance(fontprop, str):
    928-
    filename = fontprop
    930+
    filenames = [fontprop]
    929931
    elif mpl.rcParams['pdf.use14corefonts']:
    930-
    filename = findfont(
    931-
    fontprop, fontext='afm', directory=RendererPdf._afm_font_dir)
    932+
    filenames = _fontManager._find_fonts_by_props(
    933+
    fontprop, fontext='afm', directory=RendererPdf._afm_font_dir
    934+
    )
    932935
    else:
    933-
    filename = findfont(fontprop)
    934-
    935-
    Fx = self.fontNames.get(filename)
    936-
    if Fx is None:
    937-
    Fx = next(self._internal_font_seq)
    938-
    self.fontNames[filename] = Fx
    939-
    _log.debug('Assigning font %s = %r', Fx, filename)
    940-
    941-
    return Fx
    936+
    filenames = _fontManager._find_fonts_by_props(fontprop)
    937+
    first_Fx = None
    938+
    for fname in filenames:
    939+
    Fx = self.fontNames.get(fname)
    940+
    if not first_Fx:
    941+
    first_Fx = Fx
    942+
    if Fx is None:
    943+
    Fx = next(self._internal_font_seq)
    944+
    self.fontNames[fname] = Fx
    945+
    _log.debug('Assigning font %s = %r', Fx, fname)
    946+
    if not first_Fx:
    947+
    first_Fx = Fx
    948+
    949+
    # find_fontsprop's first value always adheres to
    950+
    # findfont's value, so technically no behaviour change
    951+
    return first_Fx
    942952

    943953
    def dviFontName(self, dvifont):
    944954
    """
    @@ -1204,7 +1214,6 @@ def get_char_width(charcode):
    12041214
    width = font.load_char(
    12051215
    s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance
    12061216
    return cvt(width)
    1207-
    12081217
    with warnings.catch_warnings():
    12091218
    # Ignore 'Required glyph missing from current font' warning
    12101219
    # from ft2font: here we're just building the widths table, but
    @@ -2389,22 +2398,27 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
    23892398
    # the regular text show command (TJ) with appropriate kerning between
    23902399
    # chunks, whereas multibyte characters use the XObject command (Do).
    23912400
    else:
    2392-
    # List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns.
    2401+
    # List of (ft_object, start_x, [prev_kern, char, char, ...]),
    2402+
    # w/o zero kerns.
    23932403
    singlebyte_chunks = []
    2394-
    # List of (start_x, glyph_index).
    2404+
    # List of (ft_object, start_x, glyph_index).
    23952405
    multibyte_glyphs = []
    23962406
    prev_was_multibyte = True
    2407+
    prev_font = font
    23972408
    for item in _text_helpers.layout(
    23982409
    s, font, kern_mode=KERNING_UNFITTED):
    23992410
    if _font_supports_glyph(fonttype, ord(item.char)):
    2400-
    if prev_was_multibyte:
    2401-
    singlebyte_chunks.append((item.x, []))
    2411+
    if prev_was_multibyte or item.ft_object != prev_font:
    2412+
    singlebyte_chunks.append((item.ft_object, item.x, []))
    2413+
    prev_font = item.ft_object
    24022414
    if item.prev_kern:
    2403-
    singlebyte_chunks[-1][1].append(item.prev_kern)
    2404-
    singlebyte_chunks[-1][1].append(item.char)
    2415+
    singlebyte_chunks[-1][2].append(item.prev_kern)
    2416+
    singlebyte_chunks[-1][2].append(item.char)
    24052417
    prev_was_multibyte = False
    24062418
    else:
    2407-
    multibyte_glyphs.append((item.x, item.glyph_idx))
    2419+
    multibyte_glyphs.append(
    2420+
    (item.ft_object, item.x, item.glyph_idx)
    2421+
    )
    24082422
    prev_was_multibyte = True
    24092423
    # Do the rotation and global translation as a single matrix
    24102424
    # concatenation up front
    @@ -2414,10 +2428,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
    24142428
    -math.sin(a), math.cos(a),
    24152429
    x, y, Op.concat_matrix)
    24162430
    # Emit all the 1-byte characters in a BT/ET group.
    2417-
    self.file.output(Op.begin_text,
    2418-
    self.file.fontName(prop), fontsize, Op.selectfont)
    2431+
    2432+
    self.file.output(Op.begin_text)
    24192433
    prev_start_x = 0
    2420-
    for start_x, kerns_or_chars in singlebyte_chunks:
    2434+
    for ft_object, start_x, kerns_or_chars in singlebyte_chunks:
    2435+
    ft_name = self.file.fontName(ft_object.fname)
    2436+
    self.file.output(ft_name, fontsize, Op.selectfont)
    24212437
    self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0)
    24222438
    self.file.output(
    24232439
    # See pdf spec "Text space details" for the 1000/fontsize
    @@ -2429,8 +2445,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
    24292445
    prev_start_x = start_x
    24302446
    self.file.output(Op.end_text)
    24312447
    # Then emit all the multibyte characters, one at a time.
    2432-
    for start_x, glyph_idx in multibyte_glyphs:
    2433-
    self._draw_xobject_glyph(font, fontsize, glyph_idx, start_x, 0)
    2448+
    for ft_object, start_x, glyph_idx in multibyte_glyphs:
    2449+
    self._draw_xobject_glyph(
    2450+
    ft_object, fontsize, glyph_idx, start_x, 0
    2451+
    )
    24342452
    self.file.output(Op.grestore)
    24352453

    24362454
    def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y):
    Binary file not shown.
    Binary file not shown.

    lib/matplotlib/tests/test_backend_pdf.py

    Lines changed: 29 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -9,7 +9,9 @@
    99
    import pytest
    1010

    1111
    import matplotlib as mpl
    12-
    from matplotlib import pyplot as plt, rcParams
    12+
    from matplotlib import (
    13+
    pyplot as plt, rcParams, font_manager as fm
    14+
    )
    1315
    from matplotlib.cbook import _get_data_path
    1416
    from matplotlib.ft2font import FT2Font
    1517
    from matplotlib.font_manager import findfont, FontProperties
    @@ -383,3 +385,29 @@ def test_glyphs_subset():
    383385

    384386
    # since both objects are assigned same characters
    385387
    assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs()
    388+
    389+
    390+
    @image_comparison(["multi_font_type3.pdf"])
    391+
    def test_multi_font_type3():
    392+
    fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
    393+
    if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
    394+
    pytest.skip("Font may be missing")
    395+
    396+
    plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
    397+
    plt.rc('pdf', fonttype=3)
    398+
    399+
    fig = plt.figure()
    400+
    fig.text(0.15, 0.475, "There are 几个汉字 in between!")
    401+
    402+
    403+
    @image_comparison(["multi_font_type42.pdf"])
    404+
    def test_multi_font_type42():
    405+
    fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
    406+
    if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
    407+
    pytest.skip("Font may be missing")
    408+
    409+
    plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27)
    410+
    plt.rc('pdf', fonttype=42)
    411+
    412+
    fig = plt.figure()
    413+
    fig.text(0.15, 0.475, "There are 几个汉字 in between!")

    0 commit comments

    Comments
     (0)
    0