Skip to content

Commit 7a10b51

Browse files
committed
Add TTF subsetting to backend_pdf.py.
svn path=/trunk/matplotlib/; revision=3493
1 parent ef2f42b commit 7a10b51

14 files changed

+461
-238
lines changed

lib/matplotlib/backends/backend_pdf.py

+88-21
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from cStringIO import StringIO
1616
from datetime import datetime
1717
from math import ceil, cos, floor, pi, sin
18+
import sets
1819

1920
from matplotlib import __version__, rcParams, agg, get_data_path
2021
from matplotlib._pylab_helpers import Gcf
@@ -29,6 +30,7 @@
2930
from matplotlib.mathtext import math_parse_s_pdf
3031
from matplotlib.numerix import Float32, UInt8, fromstring, arange, infinity, isnan, asarray
3132
from matplotlib.transforms import Bbox
33+
from matplotlib import ttconv
3234

3335
# Overview
3436
#
@@ -453,7 +455,8 @@ def writeFonts(self):
453455
if filename.endswith('.afm'):
454456
fontdictObject = self._write_afm_font(filename)
455457
else:
456-
fontdictObject = self.embedTTF(filename)
458+
fontdictObject = self.embedTTF(
459+
filename, self.used_characters[filename])
457460
fonts[Fx] = fontdictObject
458461
#print >>sys.stderr, filename
459462
self.writeObject(self.fontObject, fonts)
@@ -471,10 +474,11 @@ def _write_afm_font(self, filename):
471474
self.writeObject(fontdictObject, fontdict)
472475
return fontdictObject
473476

474-
def embedTTF(self, filename):
477+
def embedTTF(self, filename, characters):
475478
"""Embed the TTF font from the named file into the document."""
476479

477480
font = FT2Font(str(filename))
481+
fonttype = rcParams['pdf.fonttype']
478482

479483
def cvt(length, upe=font.units_per_EM, nearest=True):
480484
"Convert font coordinates to PDF glyph coordinates"
@@ -515,21 +519,36 @@ def get_char_width(charcode):
515519

516520
firstchar, lastchar = 0, 255
517521
widths = [ get_char_width(charcode) for charcode in range(firstchar, lastchar+1) ]
522+
font_bbox = [ cvt(x, nearest=False) for x in font.bbox ]
518523

519524
widthsObject = self.reserveObject('font widths')
520525
fontdescObject = self.reserveObject('font descriptor')
521526
# TODO: "WinAnsiEncoding" could become a parameter of PdfFile. The PDF encoding
522527
# "WinAnsiEncoding" matches the Python enconding "cp1252" used in method
523528
# RendererPdf.draw_text and RendererPdf.get_text_width_height to encode Unicode strings.
524529
fontdict = { 'Type': Name('Font'),
525-
'Subtype': Name('TrueType'),
526-
'Encoding': Name('WinAnsiEncoding'),
527530
'BaseFont': ps_name,
528531
'FirstChar': firstchar,
529532
'LastChar': lastchar,
530533
'Widths': widthsObject,
531534
'FontDescriptor': fontdescObject }
532535

536+
if fonttype == 3:
537+
charprocsObject = self.reserveObject('character procs')
538+
differencesArray = []
539+
fontdict['Subtype'] = Name('Type3')
540+
fontdict['Name'] = ps_name
541+
fontdict['FontBBox'] = font_bbox
542+
fontdict['FontMatrix'] = [ .001, 0, 0, .001, 0, 0 ]
543+
fontdict['CharProcs'] = charprocsObject
544+
fontdict['Encoding'] = {
545+
'Type': Name('Encoding'),
546+
'Differences': differencesArray}
547+
elif fonttype == 42:
548+
fontdict['Subtype'] = Name('TrueType')
549+
fontdict['Encoding'] = Name('WinAnsiEncoding'),
550+
551+
533552
flags = 0
534553
symbolic = False #ps_name.name in ('Cmsy10', 'Cmmi10', 'Cmex10')
535554
if ff & FIXED_WIDTH: flags |= 1 << 0
@@ -551,11 +570,13 @@ def get_char_width(charcode):
551570
'CapHeight': cvt(pclt['capHeight'], nearest=False),
552571
'XHeight': cvt(pclt['xHeight']),
553572
'ItalicAngle': post['italicAngle'][1], # ???
554-
'FontFile2': self.reserveObject('font file'),
555573
'MaxWidth': max(widths),
556574
'StemV': 0 # ???
557575
}
558576

577+
if fonttype == 42:
578+
descriptor['FontFile2'] = self.reserveObject('font file')
579+
559580
# Other FontDescriptor keys include:
560581
# /FontFamily /Times (optional)
561582
# /FontStretch /Normal (optional)
@@ -584,24 +605,57 @@ def get_char_width(charcode):
584605

585606
widths = [ firstchar, widths ]
586607

608+
if fonttype == 3:
609+
cmap = font.get_charmap()
610+
glyph_ids = []
611+
differences = []
612+
for c in characters:
613+
ccode = ord(c)
614+
gind = cmap.get(ccode) or 0
615+
glyph_ids.append(gind)
616+
differences.append((ccode, font.get_glyph_name(gind)))
617+
differences.sort()
618+
619+
last_c = -256
620+
for c, name in differences:
621+
if c != last_c + 1:
622+
differencesArray.append(c)
623+
differencesArray.append(Name(name))
624+
last_c = c
625+
626+
rawcharprocs = ttconv.get_pdf_charprocs(filename, glyph_ids)
627+
charprocs = {}
628+
charprocsRef = {}
629+
for charname, stream in rawcharprocs.items():
630+
charprocObject = self.reserveObject('charProc for %s' % name)
631+
self.beginStream(charprocObject.id,
632+
None,
633+
{'Length': len(stream)})
634+
self.currentstream.write(stream)
635+
self.endStream()
636+
charprocs[charname] = charprocObject
637+
self.writeObject(charprocsObject, charprocs)
638+
639+
elif fonttype == 42:
640+
length1Object = self.reserveObject('decoded length of a font')
641+
self.beginStream(descriptor['FontFile2'].id,
642+
self.reserveObject('length of font stream'),
643+
{'Length1': length1Object})
644+
fontfile = open(filename, 'rb')
645+
length1 = 0
646+
while True:
647+
data = fontfile.read(4096)
648+
if not data: break
649+
length1 += len(data)
650+
self.currentstream.write(data)
651+
fontfile.close()
652+
self.endStream()
653+
self.writeObject(length1Object, length1)
654+
587655
fontdictObject = self.reserveObject('font dictionary')
588-
length1Object = self.reserveObject('decoded length of a font')
589656
self.writeObject(fontdictObject, fontdict)
590657
self.writeObject(widthsObject, widths)
591658
self.writeObject(fontdescObject, descriptor)
592-
self.beginStream(descriptor['FontFile2'].id,
593-
self.reserveObject('length of font stream'),
594-
{'Length1': length1Object})
595-
fontfile = open(filename, 'rb')
596-
length1 = 0
597-
while True:
598-
data = fontfile.read(4096)
599-
if not data: break
600-
length1 += len(data)
601-
self.currentstream.write(data)
602-
fontfile.close()
603-
self.endStream()
604-
self.writeObject(length1Object, length1)
605659

606660
return fontdictObject
607661

@@ -849,6 +903,7 @@ def __init__(self, file):
849903
self.gc = self.new_gc()
850904
self.truetype_font_cache = {}
851905
self.afm_font_cache = {}
906+
self.file.used_characters = self.used_characters = {}
852907

853908
def finalize(self):
854909
self.gc.finalize()
@@ -865,6 +920,16 @@ def check_gc(self, gc, fillcolor=None):
865920
# Restore gc to avoid unwanted side effects
866921
gc._fillcolor = orig_fill
867922

923+
def track_characters(self, font, s):
924+
"""Keeps track of which characters are required from
925+
each font."""
926+
if isinstance(font, (str, unicode)):
927+
fname = font
928+
else:
929+
fname = font.fname
930+
used_characters = self.used_characters.setdefault(fname, sets.Set())
931+
used_characters.update(s)
932+
868933
def draw_arc(self, gcEdge, rgbFace, x, y, width, height,
869934
angle1, angle2, rotation):
870935
"""
@@ -1018,7 +1083,7 @@ def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0):
10181083
def draw_mathtext(self, gc, x, y, s, prop, angle):
10191084
# TODO: fix positioning and encoding
10201085
fontsize = prop.get_size_in_points()
1021-
width, height, pswriter = math_parse_s_pdf(s, 72, fontsize)
1086+
width, height, pswriter = math_parse_s_pdf(s, 72, fontsize, 0, self.track_characters)
10221087

10231088
self.check_gc(gc, gc._rgb)
10241089
self.file.output(Op.begin_text)
@@ -1114,6 +1179,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False):
11141179
y -= b * fontsize / 1000
11151180
else:
11161181
font = self._get_font_ttf(prop)
1182+
self.track_characters(font, s)
11171183
font.set_text(s, 0.0)
11181184
y += font.get_descent() / 64.0
11191185

@@ -1131,7 +1197,8 @@ def get_text_width_height(self, s, prop, ismath):
11311197

11321198
if ismath:
11331199
fontsize = prop.get_size_in_points()
1134-
w, h, pswriter = math_parse_s_pdf(s, 72, fontsize)
1200+
w, h, pswriter = math_parse_s_pdf(
1201+
s, 72, fontsize, 0, self.track_characters)
11351202

11361203
elif rcParams['pdf.use14corefonts']:
11371204
font = self._get_font_afm(prop)

lib/matplotlib/backends/backend_ps.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ 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.ttf2ps import convert_ttf_to_ps
23+
from matplotlib.ttconv import convert_ttf_to_ps
2424
from matplotlib.mathtext import math_parse_s_ps
2525
from matplotlib.text import Text
2626

@@ -1033,8 +1033,7 @@ def print_figure(self, outfile, dpi=72, facecolor='w', edgecolor='w',
10331033
cmap = font.get_charmap()
10341034
glyph_ids = []
10351035
for c in chars:
1036-
ccode = ord(c)
1037-
gind = cmap.get(ccode) or 0
1036+
gind = cmap.get(ord(c)) or 0
10381037
glyph_ids.append(gind)
10391038
convert_ttf_to_ps(font_filename, fh, rcParams['ps.fonttype'], glyph_ids)
10401039
print >>fh, "end"

lib/matplotlib/mathtext.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,8 @@ def render(self, ox, oy, font, sym, fontsize, dpi):
818818
fontname, metrics, glyphname, offset = \
819819
self._get_info(font, sym, fontsize, dpi)
820820
filename, num = self._get_filename_and_num(font, sym, fontsize, dpi)
821+
if self.character_tracker:
822+
self.character_tracker(filename, unichr(num))
821823
if fontname.lower() == 'cmex10':
822824
oy += offset - 512/2048.*10.
823825

@@ -1567,7 +1569,7 @@ def __call__(self, s, dpi, fontsize, angle=0, character_tracker=None):
15671569
#self.font_object = MyUnicodeFonts(output='PS')
15681570
Element.fonts = self.font_object
15691571
elif self.output == 'PDF':
1570-
self.font_object = BakomaPDFFonts()
1572+
self.font_object = BakomaPDFFonts(character_tracker)
15711573
Element.fonts = self.font_object
15721574

15731575
handler.clear()

lib/matplotlib/mpl-data/matplotlibrc

+2
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,12 @@ numerix : numpy # 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 (Type3) or Type 42 (TrueType)
261262

262263
# pdf backend params
263264
#pdf.compression : 6 # integer from 0 to 9
264265
# 0 disables compression (good for debugging)
266+
#pdf.fonttype : 3 # Output Type 3 (Type3) or Type 42 (TrueType)
265267

266268
# svg backend params
267269
#svg.image_inline : True # write raster image data directly into the svg file

lib/matplotlib/rcsetup.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,20 @@ def validate_int(s):
5353
except ValueError:
5454
raise ValueError('Could not convert "%s" to int' % s)
5555

56-
def validate_psfonttype(s):
57-
'confirm that this is a Postscript font type that we know how to convert to'
58-
fonttype = validate_int(s)
59-
if fonttype not in (3, 42):
60-
raise ValueError('Supported Postscript font types are 3 and 42')
61-
return fonttype
56+
def validate_fonttype(s):
57+
'confirm that this is a Postscript of PDF font type that we know how to convert to'
58+
fonttypes = { 'type3': 3,
59+
'truetype': 42 }
60+
try:
61+
fonttype = validate_int(s)
62+
except ValueError:
63+
if s.lower() in fonttypes.keys():
64+
return fonttypes[s.lower()]
65+
raise ValueError('Supported Postscript/PDF font types are %s' % fonttypes.keys())
66+
else:
67+
if fonttype not in fonttypes.values():
68+
raise ValueError('Supported Postscript/PDF font types are %s' % fonttypes.values())
69+
return fonttype
6270

6371
validate_backend = ValidateInStrings('backend',[
6472
'Agg2', 'Agg', 'Aqt', 'Cairo', 'CocoaAgg', 'EMF', 'GD', 'GDK',
@@ -416,11 +424,12 @@ def __call__(self, s):
416424
'ps.useafm' : [False, validate_bool], # Set PYTHONINSPECT
417425
'ps.usedistiller' : [False, validate_ps_distiller], # use ghostscript or xpdf to distill ps output
418426
'ps.distiller.res' : [6000, validate_int], # dpi
419-
'ps.fonttype' : [3, validate_psfonttype], # 3 or 42
427+
'ps.fonttype' : [3, validate_fonttype], # 3 (Type3) or 42 (Truetype)
420428
'pdf.compression' : [6, validate_int], # compression level from 0 to 9; 0 to disable
421429
'pdf.inheritcolor' : [False, validate_bool], # ignore any color-setting commands from the frontend
422430
'pdf.use14corefonts' : [False, validate_bool], # use only the 14 PDF core fonts
423431
# embedded in every PDF viewing application
432+
'pdf.fonttype' : [3, validate_fonttype], # 3 (Type3) or 42 (Truetype)
424433
'svg.image_inline' : [True, validate_bool], # write raster image data directly into the svg file
425434
'svg.image_noscale' : [False, validate_bool], # suppress scaling of raster data embedded in SVG
426435
'plugins.directory' : ['.matplotlib_plugins', str], # where plugin directory is locate

matplotlibrc.template

+2-1
Original file line numberDiff line numberDiff line change
@@ -258,11 +258,12 @@ 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)
261+
#ps.fonttype : 3 # Output Type 3 (Type3) or Type 42 (TrueType)
262262

263263
# pdf backend params
264264
#pdf.compression : 6 # integer from 0 to 9
265265
# 0 disables compression (good for debugging)
266+
#pdf.fonttype : 3 # Output Type 3 (Type3) or Type 42 (TrueType)
266267

267268
# svg backend params
268269
#svg.image_inline : True # write raster image data directly into the svg file

setup.py

+4-4
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, build_ttf2ps
85+
build_subprocess, build_isnan, build_ttconv
8686
import distutils.sysconfig
8787

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

121121
# these are not optional
122122
BUILD_FT2FONT = 1
123-
BUILD_TTF2PS = 1
123+
BUILD_TTCONV = 1
124124
BUILD_CONTOUR = 1
125125
BUILD_NXUTILS = 1
126126

@@ -270,8 +270,8 @@ def havegtk():
270270
if BUILD_FT2FONT:
271271
build_ft2font(ext_modules, packages)
272272

273-
if BUILD_TTF2PS:
274-
build_ttf2ps(ext_modules, packages)
273+
if BUILD_TTCONV:
274+
build_ttconv(ext_modules, packages)
275275

276276
if BUILD_WINDOWING and sys.platform=='win32':
277277
build_windowing(ext_modules, packages)

setupext.py

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

7474
BUILT_AGG = False
7575
BUILT_FT2FONT = False
76-
BUILT_TTF2PS = False
76+
BUILT_TTCONV = False
7777
BUILT_GTKAGG = False
7878
BUILT_IMAGE = False
7979
BUILT_TKAGG = False
@@ -501,17 +501,17 @@ def build_ft2font(ext_modules, packages):
501501
ext_modules.append(module)
502502
BUILT_FT2FONT = True
503503

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']
504+
def build_ttconv(ext_modules, packages):
505+
global BUILT_TTCONV
506+
if BUILT_TTCONV: return # only build it if you you haven't already
507+
deps = ['src/_ttconv.cpp', 'ttconv/pprdrv_tt.cpp', 'ttconv/pprdrv_tt2.cpp', 'ttconv/ttutil.cpp']
508508
deps.extend(glob.glob('CXX/*.cxx'))
509509
deps.extend(glob.glob('CXX/*.c'))
510510

511-
module = Extension('matplotlib.ttf2ps', deps)
511+
module = Extension('matplotlib.ttconv', deps)
512512
add_base_flags(module)
513513
ext_modules.append(module)
514-
BUILT_TTF2PS = True
514+
BUILT_TTCONV = True
515515

516516
def build_gtkagg(ext_modules, packages, numerix):
517517
global BUILT_GTKAGG

0 commit comments

Comments
 (0)