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

Skip to content

Commit a826065

Browse files
committed
Type-42 fonts using fontTools
Split _backend_pdf_ps.get_glyphs_subset into two functions: get_glyphs_subset returns the font object (which needs to be closed after using) and font_as_file serializes this object into a BytesIO file-like object.
1 parent 3b87792 commit a826065

File tree

4 files changed

+219
-28
lines changed

4 files changed

+219
-28
lines changed

lib/matplotlib/backends/_backend_pdf_ps.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,51 @@ def get_glyphs_subset(fontfile, characters):
2424
Subset a TTF font
2525
2626
Reads the named fontfile and restricts the font to the characters.
27-
Returns a serialization of the subset font as file-like object.
27+
Returns a TTFont object.
2828
2929
Parameters
3030
----------
3131
symbol : str
3232
Path to the font file
3333
characters : str
3434
Continuous set of characters to include in subset
35+
36+
Returns
37+
-------
38+
fontTools.ttLib.ttFont.TTFont
39+
An open font object representing the subset, which needs to
40+
be closed by the caller.
3541
"""
3642

3743
options = subset.Options(glyph_names=True, recommended_glyphs=True)
3844

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

42-
with subset.load_font(fontfile, options) as font:
43-
subsetter = subset.Subsetter(options=options)
44-
subsetter.populate(text=characters)
45-
subsetter.subset(font)
46-
fh = BytesIO()
47-
font.save(fh, reorderTables=False)
48-
return fh
48+
font = subset.load_font(fontfile, options)
49+
subsetter = subset.Subsetter(options=options)
50+
subsetter.populate(text=characters)
51+
subsetter.subset(font)
52+
return font
53+
54+
55+
def font_as_file(font):
56+
"""
57+
Convert a TTFont object into a file-like object.
58+
59+
Parameters
60+
----------
61+
font : fontTools.ttLib.ttFont.TTFont
62+
A font object
63+
64+
Returns
65+
-------
66+
BytesIO
67+
A file object with the font saved into it
68+
"""
69+
fh = BytesIO()
70+
font.save(fh, reorderTables=False)
71+
return fh
4972

5073

5174
class CharacterTracker:

lib/matplotlib/backends/backend_pdf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1229,7 +1229,8 @@ def embedTTFType42(font, characters, descriptor):
12291229

12301230
subset_str = "".join(chr(c) for c in characters)
12311231
_log.debug("SUBSET %s characters: %s", filename, subset_str)
1232-
fontdata = _backend_pdf_ps.get_glyphs_subset(filename, subset_str)
1232+
with _backend_pdf_ps.get_glyphs_subset(filename, subset_str) as subset:
1233+
fontdata = _backend_pdf_ps.font_as_file(subset)
12331234
_log.debug(
12341235
"SUBSET %s %d -> %d", filename,
12351236
os.stat(filename).st_size, fontdata.getbuffer().nbytes

lib/matplotlib/backends/backend_ps.py

Lines changed: 183 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@
22
A PostScript backend, which can produce both PostScript .ps and .eps.
33
"""
44

5+
import binascii
6+
import bisect
57
import codecs
68
import datetime
79
from enum import Enum
810
import functools
911
import glob
10-
from io import StringIO, TextIOWrapper
12+
from io import BytesIO, StringIO, TextIOWrapper
1113
import logging
1214
import math
1315
import os
1416
import pathlib
1517
import tempfile
18+
import textwrap
1619
import re
1720
import shutil
21+
import struct
1822
from tempfile import TemporaryDirectory
1923
import time
2024

25+
import fontTools
2126
import numpy as np
2227

2328
import matplotlib as mpl
@@ -29,7 +34,6 @@
2934
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
3035
from matplotlib.font_manager import get_font
3136
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE, FT2Font
32-
from matplotlib._ttconv import convert_ttf_to_ps
3337
from matplotlib.mathtext import MathTextParser
3438
from matplotlib._mathtext_data import uni2type1
3539
from matplotlib.path import Path
@@ -218,28 +222,190 @@ def _font_to_ps_type42(font_path, chars, fh):
218222
subset_str = ''.join(chr(c) for c in chars)
219223
_log.debug("SUBSET %s characters: %s", font_path, subset_str)
220224
try:
221-
fontdata = _backend_pdf_ps.get_glyphs_subset(font_path, subset_str)
222-
_log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size,
223-
fontdata.getbuffer().nbytes)
224-
225-
# Give ttconv a subsetted font along with updated glyph_ids.
226-
font = FT2Font(fontdata)
227-
glyph_ids = [font.get_char_index(c) for c in chars]
228-
with TemporaryDirectory() as tmpdir:
229-
tmpfile = os.path.join(tmpdir, "tmp.ttf")
230-
231-
with open(tmpfile, 'wb') as tmp:
232-
tmp.write(fontdata.getvalue())
233-
234-
# TODO: allow convert_ttf_to_ps to input file objects (BytesIO)
235-
convert_ttf_to_ps(os.fsencode(tmpfile), fh, 42, glyph_ids)
225+
with _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) as subset:
226+
fontdata = _backend_pdf_ps.font_as_file(subset).getvalue()
227+
_log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size,
228+
len(fontdata))
229+
fh.write(_serialize_type42(subset, fontdata))
236230
except RuntimeError:
237231
_log.warning(
238232
"The PostScript backend does not currently "
239233
"support the selected font.")
240234
raise
241235

242236

237+
def _serialize_type42(font, fontdata):
238+
"""
239+
Output a PostScript Type-42 format representation of font
240+
241+
Parameters
242+
----------
243+
font : fontTools.ttLib.ttFont.TTFont
244+
The font object
245+
fontdata : bytes
246+
The raw font data in TTF format
247+
248+
Returns
249+
-------
250+
str
251+
The Type-42 formatted font
252+
"""
253+
version, breakpoints = _version_and_breakpoints(font['loca'], fontdata)
254+
post, name = font['post'], font['name']
255+
fmt = textwrap.dedent(f"""
256+
%%!PS-TrueTypeFont-{version[0]}.{version[1]}-{font['head'].fontRevision:.7f}
257+
10 dict begin
258+
/FontType 42 def
259+
/FontMatrix [1 0 0 1 0 0] def
260+
/FontName /{name.getDebugName(6)} def
261+
/FontInfo 7 dict dup begin
262+
/FullName ({name.getDebugName(4)}) def
263+
/FamilyName ({name.getDebugName(1)}) def
264+
/Version ({name.getDebugName(5)}) def
265+
/ItalicAngle {post.italicAngle} def
266+
/isFixedPitch {'true' if post.isFixedPitch else 'false'} def
267+
/UnderlinePosition {post.underlinePosition} def
268+
/UnderlineThickness {post.underlineThickness} def
269+
end readonly def
270+
/Encoding StandardEncoding def
271+
/FontBBox [{' '.join(str(x) for x in _bounds(font))}] def
272+
/PaintType 0 def
273+
/CIDMap 0 def
274+
%s
275+
%s
276+
FontName currentdict end definefont pop
277+
""")
278+
279+
return fmt % (_charstrings(font), _sfnts(fontdata, font, breakpoints))
280+
281+
282+
def _version_and_breakpoints(loca, fontdata):
283+
"""
284+
Read the version number of the font and determine sfnts breakpoints.
285+
When a TrueType font file is written as a Type 42 font, it has to be
286+
broken into substrings of at most 65535 bytes. These substrings must
287+
begin at font table boundaries or glyph boundaries in the glyf table.
288+
This function determines all possible breakpoints and it is the caller's
289+
responsibility to do the splitting.
290+
291+
Helper function for _font_to_ps_type42.
292+
293+
Parameters
294+
----------
295+
loca : fontTools.ttLib._l_o_c_a.table__l_o_c_a
296+
The loca table of the font
297+
fontdata : bytes
298+
The raw data of the font
299+
300+
Returns
301+
-------
302+
tuple
303+
((v1, v2), breakpoints) where v1 is the major version number,
304+
v2 is the minor version number and breakpoints is a sorted list
305+
of offsets into fontdata
306+
"""
307+
v1, v2, numTables = struct.unpack('>3h', fontdata[:6])
308+
version = (v1, v2)
309+
310+
tables = {}
311+
for i in range(numTables):
312+
tag, _, offset, _ = struct.unpack('>4sIII', fontdata[12 + i*16:12 + (i+1)*16])
313+
tables[tag.decode('ascii')] = offset
314+
breakpoints = sorted(
315+
set(tables.values())
316+
| { tables['glyf'] + offset for offset in loca.locations[:-1] }
317+
| { len(fontdata) }
318+
)
319+
320+
return version, breakpoints
321+
322+
323+
def _bounds(font):
324+
"""
325+
Compute the font bounding box, as if all glyphs were written
326+
at the same start position.
327+
328+
Helper function for _font_to_ps_type42.
329+
330+
Parameters
331+
----------
332+
font : fontTools.ttLib.ttFont.TTFont
333+
The font
334+
335+
Returns
336+
-------
337+
tuple
338+
(xMin, yMin, xMax, yMax) of the combined bounding box
339+
of all the glyphs in the font
340+
"""
341+
gs = font.getGlyphSet(False)
342+
pen = fontTools.pens.boundsPen.BoundsPen(gs)
343+
for name in gs.keys():
344+
gs[name].draw(pen)
345+
return pen.bounds
346+
347+
348+
def _charstrings(font):
349+
"""
350+
Transform font glyphs into CharStrings
351+
352+
Helper function for _font_to_ps_type42.
353+
354+
Parameters
355+
----------
356+
font : fontTools.ttLib.ttFont.TTFont
357+
The font
358+
359+
Returns
360+
-------
361+
str
362+
A definition of the CharStrings dictionary in PostScript
363+
"""
364+
s = StringIO()
365+
go = font.getGlyphOrder()
366+
s.write(f'/CharStrings {len(go)} dict dup begin\n')
367+
for i, name in enumerate(go):
368+
s.write(f'/{name} {i} def\n')
369+
s.write('end readonly def')
370+
return s.getvalue()
371+
372+
373+
def _sfnts(fontdata, font, breakpoints):
374+
"""
375+
Transform font data into PostScript sfnts format.
376+
377+
Helper function for _font_to_ps_type42.
378+
379+
Parameters
380+
----------
381+
fontdata : bytes
382+
The raw data of the font
383+
font : fontTools.ttLib.ttFont.TTFont
384+
The fontTools font object
385+
breakpoints : list
386+
Sorted offsets of possible breakpoints
387+
388+
Returns
389+
-------
390+
str
391+
The sfnts array for the font definition, consisting
392+
of hex-encoded strings in PostScript format
393+
"""
394+
b = BytesIO()
395+
b.write(b'/sfnts[')
396+
pos = 0
397+
while pos < len(fontdata):
398+
i = bisect.bisect_left(breakpoints, pos + 65534)
399+
newpos = breakpoints[i-1]
400+
b.write(b'<')
401+
b.write(binascii.hexlify(fontdata[pos:newpos]))
402+
b.write(b'00>') # need an extra zero byte on every string
403+
pos = newpos
404+
b.write(b']def')
405+
s = b.getvalue().decode('ascii')
406+
return '\n'.join(s[i:i+100] for i in range(0, len(s), 100))
407+
408+
243409
def _log_if_debug_on(meth):
244410
"""
245411
Wrap `RendererPS` method *meth* to emit a PS comment with the method name,

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from matplotlib import dviread, pyplot as plt, checkdep_usetex, rcParams
1313
from matplotlib.cbook import _get_data_path
1414
from matplotlib.ft2font import FT2Font
15-
from matplotlib.backends._backend_pdf_ps import get_glyphs_subset
15+
from matplotlib.backends._backend_pdf_ps import get_glyphs_subset, font_as_file
1616
from matplotlib.backends.backend_pdf import PdfPages
1717

1818
from matplotlib.testing.decorators import check_figures_equal, image_comparison
@@ -354,7 +354,8 @@ def test_glyphs_subset():
354354
nosubfont.set_text(chars)
355355

356356
# subsetted FT2Font
357-
subfont = FT2Font(get_glyphs_subset(fpath, chars))
357+
with get_glyphs_subset(fpath, chars) as subset:
358+
subfont = FT2Font(font_as_file(subset))
358359
subfont.set_text(chars)
359360

360361
nosubcmap = nosubfont.get_charmap()

0 commit comments

Comments
 (0)