Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement multi-font embedding for PDF Backend #20804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d0906aa
Parse fallback_list through wrapper
aitikgupta Jul 25, 2021
f6f2fd7
Define new/modify previous FT2Font functions
aitikgupta Jul 25, 2021
5199773
Implement new/modify previous FT2Font functions
aitikgupta Jul 25, 2021
88da97f
Parse multiple fonts for a single FT2Font object
aitikgupta Jul 25, 2021
c6c3a45
Trigger font fallback for Agg backend
aitikgupta Jul 25, 2021
e868fbc
Remove prints
aitikgupta Aug 7, 2021
4230469
Cleanup wrapper
aitikgupta Aug 7, 2021
991c796
Remove stale prints
aitikgupta Aug 7, 2021
fa385eb
Do not warn for get_char_index
aitikgupta Aug 7, 2021
471ae8c
Left != Right kerning comment
aitikgupta Aug 7, 2021
86878c8
Windows compiler fix
aitikgupta Aug 7, 2021
5ccb7de
Add fallback test for Agg backend
aitikgupta Aug 13, 2021
ed495d3
Debug fontNames
aitikgupta Jul 28, 2021
d87ada8
Segfaults on exit
aitikgupta Jul 29, 2021
adebae4
More work on PDF backend
aitikgupta Jul 31, 2021
9983c66
Implement another approach
aitikgupta Aug 1, 2021
2ef863e
Revisit the approach
aitikgupta Aug 3, 2021
f12c295
Type3 PDF Backend works!
aitikgupta Aug 6, 2021
1327b82
Type42 PDF fallback works!
aitikgupta Aug 6, 2021
799ffde
Create a fill_glyphs method
aitikgupta Aug 7, 2021
7ce1805
Use fill_glyphs instead of set_text
aitikgupta Aug 7, 2021
ac1c9c9
Cleanup wrapper
aitikgupta Aug 7, 2021
8a30aaf
Few cleanups
aitikgupta Aug 7, 2021
e675047
Rebase from Agg backend
aitikgupta Aug 7, 2021
1b53070
Specify font number for TTC fonts
aitikgupta Aug 13, 2021
0e0d92c
Flake8 fixes
aitikgupta Aug 13, 2021
cb50510
Add multi-font tests for PDF backend
aitikgupta Aug 13, 2021
70b0e18
Add baseline images
aitikgupta Aug 13, 2021
42b6d6f
Merge branch 'master' into pdf-fallback
aitikgupta Aug 13, 2021
8fd0729
Fix memory leak and render tofu
aitikgupta Aug 17, 2021
7c88286
Check if fallback font is available
aitikgupta Aug 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 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 @@ -63,6 +63,7 @@ def layout(string, font, *, kern_mode=KERNING_DEFAULT):
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)
ft_object = font.get_glyph_to_font().get(glyph_idx, font)
yield LayoutItem(ft_object, char, glyph_idx, x, kern)
x += glyph.linearHoriAdvance / 65536
prev_glyph_idx = glyph_idx
9 changes: 7 additions & 2 deletions lib/matplotlib/backends/_backend_pdf_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Common functionality between the PDF and PS backends.
"""

import os
from io import BytesIO
import functools

Expand Down Expand Up @@ -37,7 +38,11 @@ def get_glyphs_subset(fontfile, characters):
options = subset.Options(glyph_names=True, recommended_glyphs=True)

# prevent subsetting FontForge Timestamp and other tables
options.drop_tables += ['FFTM', 'PfEd']
options.drop_tables += ['FFTM', 'PfEd', 'BDF']

# if fontfile is a ttc, specify font number
if os.path.splitext(fontfile)[1] == ".ttc":
options.font_number = 0

with subset.load_font(fontfile, options) as font:
subsetter = subset.Subsetter(options=options)
Expand Down Expand Up @@ -136,7 +141,7 @@ def _get_font_afm(self, prop):
return _cached_get_afm_from_fname(fname)

def _get_font_ttf(self, prop):
fname = font_manager.findfont(prop)
fname = font_manager.find_fontsprop(prop)
font = font_manager.get_font(fname)
font.clear()
font.set_size(prop.get_size_in_points(), 72)
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
RendererBase)
from matplotlib.font_manager import findfont, get_font
from matplotlib.font_manager import find_fontsprop, get_font
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
from matplotlib.mathtext import MathTextParser
Expand Down Expand Up @@ -251,7 +251,7 @@ def _get_agg_font(self, prop):
"""
Get the font for text instance t, caching for efficiency
"""
fname = findfont(prop)
fname = find_fontsprop(prop)
font = get_font(fname)

font.clear()
Expand Down
78 changes: 48 additions & 30 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
GraphicsContextBase, 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, find_fontsprop, get_font
from matplotlib.afm import AFM
import matplotlib.type1font as type1font
import matplotlib.dviread as dviread
Expand Down Expand Up @@ -861,20 +861,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 = find_fontsprop(
fontprop, fontext='afm', directory=RendererPdf._afm_font_dir
).values()
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 = find_fontsprop(fontprop).values()
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 @@ -1143,7 +1151,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 @@ -2331,7 +2338,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
fonttype = 1
else:
font = self._get_font_ttf(prop)
self.file._character_tracker.track(font, s)
char_to_font = font.fill_glyphs(s)
for char, font in char_to_font.items():
self.file._character_tracker.track(font, chr(char))
fonttype = mpl.rcParams['pdf.fonttype']

if gc.get_url() is not None:
Expand Down Expand Up @@ -2371,22 +2380,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)
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 @@ -2396,10 +2410,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 @@ -2411,14 +2427,16 @@ 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):
def _draw_xobject_glyph(self, ft_object, fontsize, glyph_idx, x, y):
"""Draw a multibyte character from a Type 3 font as an XObject."""
symbol_name = font.get_glyph_name(glyph_idx)
name = self.file._get_xobject_symbol_name(font.fname, symbol_name)
symbol_name = ft_object.get_glyph_name(glyph_idx)
name = self.file._get_xobject_symbol_name(ft_object.fname, symbol_name)
self.file.output(
Op.gsave,
0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix,
Expand Down
138 changes: 133 additions & 5 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# - setWeights function needs improvement
# - 'light' is an invalid weight value, remove it.

from collections import OrderedDict
import dataclasses
from functools import lru_cache
import json
Expand Down Expand Up @@ -1308,6 +1309,117 @@ def findfont(self, prop, fontext='ttf', directory=None,
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
rc_params)

def find_fontsprop(self, prop, fontext='ttf', directory=None,
fallback_to_default=True, rebuild_if_missing=True):
"""
Find font families that most closely matches the given properties.

Parameters
----------
prop : str or `~matplotlib.font_manager.FontProperties`
The font properties to search for. This can be either a
`.FontProperties` object or a string defining a
`fontconfig patterns`_.

fontext : {'ttf', 'afm'}, default: 'ttf'
The extension of the font file:

- 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf)
- 'afm': Adobe Font Metrics (.afm)

directory : str, optional
If given, only search this directory and its subdirectories.

fallback_to_default : bool
If True, will fallback to the default font family (usually
"DejaVu Sans" or "Helvetica") if none of the families were found.

rebuild_if_missing : bool
Whether to rebuild the font cache and search again if the first
match appears to point to a nonexisting font (i.e., the font cache
contains outdated entries).

Returns
-------
OrderedDict
key, value pair of families and their corresponding filepaths.

Notes
-----
This is a plugin to original findfont API, which only returns a
single font for given font properties. Instead, this API returns
an OrderedDict containing multiple fonts and their filepaths which
closely match the given font properties.
Since this internally uses original API, there's no change
to the logic of performing the nearest neighbor search.
See `findfont` for more details.

"""
# print("finding font!")

rc_params = tuple(tuple(rcParams[key]) for key in [
"font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
"font.monospace"])

prop = FontProperties._from_any(prop)
ffamily = prop.get_family()

fpaths = OrderedDict()
for fidx in range(len(ffamily)):
cprop = prop.copy()

# set current prop's family
cprop.set_family(ffamily[fidx])

# do not fall back to default font
fpath = self._findfontsprop_cached(
ffamily[fidx], cprop, fontext, directory,
False, rebuild_if_missing, rc_params
)
if fpath:
fpaths[ffamily[fidx]] = fpath

# only add default family if no other font was found
# and fallback_to_default is enabled
if not fpaths:
if fallback_to_default:
dfamily = self.defaultFamily[fontext]
cprop = prop.copy().set_family(dfamily)
fpath = self._findfontsprop_cached(
dfamily, cprop, fontext, directory,
True, rebuild_if_missing, rc_params
)
fpaths[dfamily] = fpath
else:
raise ValueError("Failed to find any font, and fallback "
"to the default font was disabled.")

return fpaths

@lru_cache()
def _findfontsprop_cached(
self, family, prop, fontext, directory,
fallback_to_default, rebuild_if_missing, rc_params
):
try:
return self._findfont_cached(
prop, fontext, directory, fallback_to_default,
rebuild_if_missing, rc_params
)
except ValueError:
if not fallback_to_default:
if family.lower() in font_family_aliases:
_log.warning(
"findfont: Generic family %r not found because "
"none of the following families were found: %s",
family,
", ".join(self._expand_aliases(family))
)
else:
_log.warning(
'findfont: Font family \'%s\' not found.', family
)

@lru_cache()
def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
rebuild_if_missing, rc_params):
Expand Down Expand Up @@ -1401,9 +1513,21 @@ def is_opentype_cff_font(filename):


@lru_cache(64)
def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
return ft2font.FT2Font(
filename, hinting_factor, _kerning_factor=_kerning_factor)
def _get_font(fpaths, hinting_factor, *, _kerning_factor, thread_id):
ftobjects = []
for fpath in fpaths[1:]:
ftobject = ft2font.FT2Font(
fpath, hinting_factor,
_kerning_factor=_kerning_factor
)
ftobjects.append(ftobject)

ft2font_object = ft2font.FT2Font(
fpaths[0], hinting_factor,
_fallback_list=ftobjects,
_kerning_factor=_kerning_factor
)
return ft2font_object


# FT2Font objects cannot be used across fork()s because they reference the same
Expand All @@ -1417,11 +1541,14 @@ def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id):
def get_font(filename, hinting_factor=None):
# Resolving the path avoids embedding the font twice in pdf/ps output if a
# single font is selected using two different relative paths.
filename = _cached_realpath(filename)
if isinstance(filename, OrderedDict):
paths = tuple(_cached_realpath(fname) for fname in filename.values())
else:
paths = (_cached_realpath(filename),)
if hinting_factor is None:
hinting_factor = rcParams['text.hinting_factor']
# also key on the thread ID to prevent segfaults with multi-threading
return _get_font(filename, hinting_factor,
return _get_font(paths, hinting_factor,
_kerning_factor=rcParams['text.kerning_factor'],
thread_id=threading.get_ident())

Expand All @@ -1446,3 +1573,4 @@ def _load_fontmanager(*, try_read_cache=True):

fontManager = _load_fontmanager()
findfont = fontManager.findfont
find_fontsprop = fontManager.find_fontsprop
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading