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

Skip to content

Commit 447fd1b

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 447fd1b

File tree

4 files changed

+207
-30
lines changed

4 files changed

+207
-30
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: 171 additions & 19 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,27 +222,175 @@ 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)
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))
230+
except RuntimeError:
231+
print("XXX too bad")
232+
raise
224233

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")
230234

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

243395
def _log_if_debug_on(meth):
244396
"""

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)