Skip to content

Commit a14a4b7

Browse files
committed
First pass of Type 3 font subsetting for Postscript backend. Made
ttconv code exception and threadsafe. Added subsetting. svn path=/trunk/matplotlib/; revision=3490
1 parent 8ee82f0 commit a14a4b7

16 files changed

+853
-1299
lines changed

lib/matplotlib/backends/backend_ps.py

+32-100
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def _fn_name(): return sys._getframe(1).f_code.co_name
2020

2121
from matplotlib.font_manager import fontManager
2222
from matplotlib.ft2font import FT2Font, KERNING_UNFITTED, KERNING_DEFAULT, KERNING_UNSCALED
23-
from matplotlib.mathtext import math_parse_s_ps, bakoma_fonts
23+
from matplotlib.ttf2ps import convert_ttf_to_ps
24+
from matplotlib.mathtext import math_parse_s_ps
2425
from matplotlib.text import Text
2526

2627
from matplotlib.transforms import get_vec6_scales
@@ -29,6 +30,7 @@ def _fn_name(): return sys._getframe(1).f_code.co_name
2930
fromstring, nonzero, ones, put, take, where, isnan
3031
import binascii
3132
import re
33+
import sets
3234

3335
if sys.platform.startswith('win'): cmd_split = '&'
3436
else: cmd_split = ';'
@@ -97,11 +99,6 @@ def quote_ps_string(s):
9799
return s
98100

99101

100-
_fontd = {}
101-
_afmfontd = {}
102-
_type42 = []
103-
104-
105102
def seq_allequal(seq1, seq2):
106103
"""
107104
seq1 and seq2 are either None or sequences or numerix arrays
@@ -145,6 +142,17 @@ def __init__(self, width, height, pswriter, dpi=72):
145142
self.hatch = None
146143
self.image_magnification = dpi/72.0
147144

145+
self.fontd = {}
146+
self.afmfontd = {}
147+
self.used_characters = {}
148+
149+
def track_characters(self, font, s):
150+
"""Keeps track of which characters are required from
151+
each font."""
152+
fname = font.fname
153+
used_characters = self.used_characters.setdefault(fname, sets.Set())
154+
used_characters.update(s)
155+
148156
def set_color(self, r, g, b, store=1):
149157
if (r,g,b) != self.color:
150158
if r==g and r==b:
@@ -264,7 +272,7 @@ def get_text_width_height(self, s, prop, ismath):
264272

265273
if ismath:
266274
width, height, pswriter = math_parse_s_ps(
267-
s, 72, prop.get_size_in_points())
275+
s, 72, prop.get_size_in_points(), 0, self.track_characters)
268276
return width, height
269277

270278
if rcParams['ps.useafm']:
@@ -291,21 +299,19 @@ def flipy(self):
291299

292300
def _get_font_afm(self, prop):
293301
key = hash(prop)
294-
font = _afmfontd.get(key)
302+
font = self.afmfontd.get(key)
295303
if font is None:
296304
font = AFM(file(fontManager.findfont(prop, fontext='afm')))
297-
_afmfontd[key] = font
305+
self.afmfontd[key] = font
298306
return font
299307

300308
def _get_font_ttf(self, prop):
301309
key = hash(prop)
302-
font = _fontd.get(key)
310+
font = self.fontd.get(key)
303311
if font is None:
304312
fname = fontManager.findfont(prop)
305313
font = FT2Font(str(fname))
306-
_fontd[key] = font
307-
if fname not in _type42:
308-
_type42.append(fname)
314+
self.fontd[key] = font
309315
font.clear()
310316
size = prop.get_size_in_points()
311317
font.set_size(size, 72.0)
@@ -727,6 +733,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath):
727733
else:
728734
font = self._get_font_ttf(prop)
729735
font.set_text(s,0)
736+
self.track_characters(font, s)
730737

731738
self.set_color(*gc.get_rgb())
732739
self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points())
@@ -753,6 +760,7 @@ def draw_unicode(self, gc, x, y, s, prop, angle):
753760

754761
self.set_color(*gc.get_rgb())
755762
self.set_font(font.get_sfnt()[(1,0,0,6)], prop.get_size_in_points())
763+
self.track_characters(font, s)
756764

757765
cmap = font.get_charmap()
758766
lastgind = None
@@ -800,7 +808,7 @@ def draw_mathtext(self, gc,
800808
self._pswriter.write("% mathtext\n")
801809

802810
fontsize = prop.get_size_in_points()
803-
width, height, pswriter = math_parse_s_ps(s, 72, fontsize)
811+
width, height, pswriter = math_parse_s_ps(s, 72, fontsize, angle, self.track_characters)
804812
self.set_color(*gc.get_rgb())
805813
thetext = pswriter.getvalue()
806814
ps = """gsave
@@ -873,7 +881,6 @@ def push_gc(self, gc, store=1):
873881

874882
## write("\n")
875883

876-
877884
class GraphicsContextPS(GraphicsContextBase):
878885
def get_capstyle(self):
879886
return {'butt':0,
@@ -893,85 +900,6 @@ def new_figure_manager(num, *args, **kwargs):
893900
manager = FigureManagerPS(canvas, num)
894901
return manager
895902

896-
def encodeTTFasPS(fontfile):
897-
"""
898-
Encode a TrueType font file for embedding in a PS file.
899-
"""
900-
font = file(fontfile, 'rb')
901-
hexdata, data = [], font.read(65520)
902-
b2a_hex = binascii.b2a_hex
903-
while data:
904-
hexdata.append('<%s>\n' %
905-
'\n'.join([b2a_hex(data[j:j+36]).upper()
906-
for j in range(0, len(data), 36)]) )
907-
data = font.read(65520)
908-
909-
hexdata = ''.join(hexdata)[:-2] + '00>'
910-
font = FT2Font(str(fontfile))
911-
912-
headtab = font.get_sfnt_table('head')
913-
version = '%d.%d' % headtab['version']
914-
revision = '%d.%d' % headtab['fontRevision']
915-
916-
dictsize = 8
917-
fontname = font.postscript_name
918-
encoding = 'StandardEncoding'
919-
fontbbox = '[%d %d %d %d]' % font.bbox
920-
921-
posttab = font.get_sfnt_table('post')
922-
minmemory= posttab['minMemType42']
923-
maxmemory= posttab['maxMemType42']
924-
925-
infosize = 7
926-
sfnt = font.get_sfnt()
927-
notice = sfnt[(1,0,0,0)]
928-
family = sfnt[(1,0,0,1)]
929-
fullname = sfnt[(1,0,0,4)]
930-
iversion = sfnt[(1,0,0,5)]
931-
fixpitch = str(bool(posttab['isFixedPitch'])).lower()
932-
ulinepos = posttab['underlinePosition']
933-
ulinethk = posttab['underlineThickness']
934-
italicang= '(%d.%d)' % posttab['italicAngle']
935-
936-
numglyphs = font.num_glyphs
937-
glyphs = []
938-
for j in range(numglyphs):
939-
glyphs.append('/%s %d def' % (font.get_glyph_name(j), j))
940-
if j != 0 and j%4 == 0:
941-
glyphs.append('\n')
942-
else:
943-
glyphs.append(' ')
944-
glyphs = ''.join(glyphs)
945-
data = ['%%!PS-TrueType-%(version)s-%(revision)s\n' % locals()]
946-
if maxmemory:
947-
data.append('%%%%VMusage: %(minmemory)d %(maxmemory)d' % locals())
948-
data.append("""%(dictsize)d dict begin
949-
/FontName /%(fontname)s def
950-
/FontMatrix [1 0 0 1 0 0] def
951-
/FontType 42 def
952-
/Encoding %(encoding)s def
953-
/FontBBox %(fontbbox)s def
954-
/PaintType 0 def
955-
/FontInfo %(infosize)d dict dup begin
956-
/Notice (%(notice)s) def
957-
/FamilyName (%(family)s) def
958-
/FullName (%(fullname)s) def
959-
/version (%(iversion)s) def
960-
/isFixedPitch %(fixpitch)s def
961-
/UnderlinePosition %(ulinepos)s def
962-
/UnderlineThickness %(ulinethk)s def
963-
end readonly def
964-
/sfnts [
965-
%(hexdata)s
966-
] def
967-
/CharStrings %(numglyphs)d dict dup begin
968-
%(glyphs)s
969-
end readonly def
970-
FontName currentdict end definefont pop""" % locals())
971-
return ''.join(data)
972-
973-
974-
975903
class FigureCanvasPS(FigureCanvasBase):
976904
def draw(self):
977905
pass
@@ -1091,20 +1019,24 @@ def print_figure(self, outfile, dpi=72, facecolor='w', edgecolor='w',
10911019

10921020
Ndict = len(psDefs)
10931021
print >>fh, "%%BeginProlog"
1094-
type42 = _type42 + bakoma_fonts
10951022
if not rcParams['ps.useafm']:
1096-
Ndict += len(type42)
1023+
Ndict += len(renderer.used_characters)
10971024
print >>fh, "/mpldict %d dict def"%Ndict
10981025
print >>fh, "mpldict begin"
10991026
for d in psDefs:
11001027
d=d.strip()
11011028
for l in d.split('\n'):
11021029
print >>fh, l.strip()
11031030
if not rcParams['ps.useafm']:
1104-
for font in type42:
1105-
print >>fh, "%%BeginFont: "+FT2Font(str(font)).postscript_name
1106-
print >>fh, encodeTTFasPS(font)
1107-
print >>fh, "%%EndFont"
1031+
for font_filename, chars in renderer.used_characters.items():
1032+
font = FT2Font(font_filename)
1033+
cmap = font.get_charmap()
1034+
glyph_ids = []
1035+
for c in chars:
1036+
ccode = ord(c)
1037+
gind = cmap.get(ccode) or 0
1038+
glyph_ids.append(gind)
1039+
convert_ttf_to_ps(font_filename, fh, rcParams['ps.fonttype'], glyph_ids)
11081040
print >>fh, "end"
11091041
print >>fh, "%%EndProlog"
11101042

lib/matplotlib/mathtext.py

+8-19
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,6 @@
146146
from matplotlib.numerix import absolute
147147
from matplotlib import get_data_path, rcParams
148148

149-
bakoma_fonts = []
150-
151149
# symbols that have the sub and superscripts over/under
152150
overunder = { r'\sum' : 1,
153151
r'\int' : 1,
@@ -416,12 +414,6 @@ def _get_info(self, facename, symbol, fontsize, dpi):
416414
if tup is not None:
417415
return tup
418416
filename = self.filenamesd[facename]
419-
# This is used only by the PS backend --- to integrate the fonts
420-
# into the resulting PS. TO-DO: fix the PS backend so it doesn't do
421-
# these dirty tricks
422-
if self.output == 'PS':
423-
if filename not in bakoma_fonts:
424-
bakoma_fonts.append(filename)
425417
fontface = self.fonts[facename]
426418
fontface.set_size(fontsize, dpi)
427419
head = fontface.get_sfnt_table('head')
@@ -510,11 +502,6 @@ def _get_info(self, facename, symbol, fontsize, dpi):
510502
if tup is not None:
511503
return tup
512504
filename = self.filenamesd[facename]
513-
# This is used only by the PS backend --- to integrate the fonts
514-
# into the resulting PS.
515-
if self.output == 'PS':
516-
if filename not in bakoma_fonts:
517-
bakoma_fonts.append(filename)
518505
fontface = self.fonts[facename]
519506
fontface.set_size(fontsize, dpi)
520507
head = fontface.get_sfnt_table('head')
@@ -718,7 +705,7 @@ class BakomaPSFonts(Fonts):
718705
None : 'cmmi10',
719706
}
720707

721-
def __init__(self):
708+
def __init__(self, character_tracker=None):
722709
self.glyphd = {}
723710
self.fonts = dict(
724711
[ (name, FT2Font(os.path.join(self.basepath, name) + '.ttf'))
@@ -731,6 +718,7 @@ def __init__(self):
731718
for charcode, glyphind in charmap.items()])
732719
for font in self.fonts.values():
733720
font.clear()
721+
self.character_tracker = character_tracker
734722

735723
def _get_info (self, font, sym, fontsize, dpi):
736724
'load the cmfont, metrics and glyph with caching'
@@ -752,14 +740,15 @@ def _get_info (self, font, sym, fontsize, dpi):
752740
num = 0
753741
#sym = '.notdef'
754742
raise ValueError('unrecognized symbol "%s, %d"' % (sym, num))
755-
filename = os.path.join(self.basepath, basename) + '.ttf'
756-
if filename not in bakoma_fonts:
757-
bakoma_fonts.append(filename)
743+
758744
cmfont = self.fonts[basename]
759745
cmfont.set_size(fontsize, dpi)
760746
head = cmfont.get_sfnt_table('head')
761747
glyph = cmfont.load_char(num)
762748

749+
if self.character_tracker:
750+
self.character_tracker(cmfont, unichr(num))
751+
763752
xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
764753
if basename == 'cmex10':
765754
offset = -(head['yMin']+512)/head['unitsPerEm']*10.
@@ -1555,7 +1544,7 @@ def __init__(self, output):
15551544
self.output = output
15561545
self.cache = {}
15571546

1558-
def __call__(self, s, dpi, fontsize, angle=0):
1547+
def __call__(self, s, dpi, fontsize, angle=0, character_tracker=None):
15591548
cacheKey = (s, dpi, fontsize, angle)
15601549
s = s[1:-1] # strip the $ from front and back
15611550
if self.cache.has_key(cacheKey):
@@ -1574,7 +1563,7 @@ def __call__(self, s, dpi, fontsize, angle=0):
15741563
self.font_object = StandardPSFonts()
15751564
Element.fonts = self.font_object
15761565
else:
1577-
self.font_object = BakomaPSFonts()
1566+
self.font_object = BakomaPSFonts(character_tracker)
15781567
#self.font_object = MyUnicodeFonts(output='PS')
15791568
Element.fonts = self.font_object
15801569
elif self.output == 'PDF':

matplotlibrc.template

+1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ numerix : %(numerix)s # numpy, Numeric or numarray
258258
# xpdf intended for production of publication quality files,
259259
# but requires ghostscript, xpdf and ps2eps
260260
#ps.distiller.res : 6000 # dpi
261+
#ps.fonttype : 3 # Output Type 3 (raw Postscript) or Type 42 (embedded TrueType)
261262

262263
# pdf backend params
263264
#pdf.compression : 6 # integer from 0 to 9

setup.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
from setupext import build_agg, build_gtkagg, build_tkagg, build_wxagg,\
8383
build_ft2font, build_image, build_windowing, build_transforms, \
8484
build_contour, build_nxutils, build_enthought, build_swigagg, build_gdk, \
85-
build_subprocess, build_isnan
85+
build_subprocess, build_isnan, build_ttf2ps
8686
import distutils.sysconfig
8787

8888
for line in file('lib/matplotlib/__init__.py').readlines():
@@ -120,6 +120,7 @@
120120

121121
# these are not optional
122122
BUILD_FT2FONT = 1
123+
BUILD_TTF2PS = 1
123124
BUILD_CONTOUR = 1
124125
BUILD_NXUTILS = 1
125126

@@ -269,6 +270,9 @@ def havegtk():
269270
if BUILD_FT2FONT:
270271
build_ft2font(ext_modules, packages)
271272

273+
if BUILD_TTF2PS:
274+
build_ttf2ps(ext_modules, packages)
275+
272276
if BUILD_WINDOWING and sys.platform=='win32':
273277
build_windowing(ext_modules, packages)
274278

setupext.py

+13
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373

7474
BUILT_AGG = False
7575
BUILT_FT2FONT = False
76+
BUILT_TTF2PS = False
7677
BUILT_GTKAGG = False
7778
BUILT_IMAGE = False
7879
BUILT_TKAGG = False
@@ -500,6 +501,18 @@ def build_ft2font(ext_modules, packages):
500501
ext_modules.append(module)
501502
BUILT_FT2FONT = True
502503

504+
def build_ttf2ps(ext_modules, packages):
505+
global BUILT_TTF2PS
506+
if BUILT_TTF2PS: return # only build it if you you haven't already
507+
deps = ['src/_ttf2ps.cpp', 'ttconv/pprdrv_tt.cpp', 'ttconv/pprdrv_tt2.cpp', 'ttconv/ttutil.cpp']
508+
deps.extend(glob.glob('CXX/*.cxx'))
509+
deps.extend(glob.glob('CXX/*.c'))
510+
511+
module = Extension('matplotlib.ttf2ps', deps)
512+
add_base_flags(module)
513+
ext_modules.append(module)
514+
BUILT_TTF2PS = True
515+
503516
def build_gtkagg(ext_modules, packages, numerix):
504517
global BUILT_GTKAGG
505518
if BUILT_GTKAGG: return # only build it if you you haven't already

0 commit comments

Comments
 (0)