Skip to content

Commit 4514212

Browse files
committed
Call kpsewhich with more arguments at one time
This should improve performance if there is a significant startup cost to running kpsewhich, as reported by some users in #4880.
1 parent e987a58 commit 4514212

File tree

4 files changed

+144
-10
lines changed

4 files changed

+144
-10
lines changed

lib/matplotlib/dviread.py

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from six.moves import xrange
2424

2525
from collections import namedtuple
26-
from contextlib import closing
2726
from functools import partial, wraps
2827
import logging
2928
import numpy as np
@@ -185,8 +184,7 @@ def wrapper(self, byte):
185184
class Dvi(object):
186185
"""
187186
A reader for a dvi ("device-independent") file, as produced by TeX.
188-
The current implementation can only iterate through pages in order,
189-
and does not even attempt to verify the postamble.
187+
The current implementation can only iterate through pages in order.
190188
191189
This class can be used as a context manager to close the underlying
192190
file upon exit. Pages can be read via iteration. Here is an overly
@@ -195,33 +193,55 @@ class Dvi(object):
195193
>>> with matplotlib.dviread.Dvi('input.dvi', 72) as dvi:
196194
>>> for page in dvi:
197195
>>> print ''.join(unichr(t.glyph) for t in page.text)
196+
197+
Parameters
198+
----------
199+
200+
filename : str
201+
dvi file to read
202+
dpi : number or None
203+
Dots per inch, can be floating-point; this affects the
204+
coordinates returned. Use None to get TeX's internal units
205+
which are likely only useful for debugging.
206+
cache : _tex_support_cache instance, optional
207+
Support file cache instance, defaults to the _tex_support_cache
208+
singleton.
198209
"""
199210
# dispatch table
200211
_dtable = [None for _ in xrange(256)]
201212
dispatch = partial(_dispatch, _dtable)
202213

203-
def __init__(self, filename, dpi):
214+
def __init__(self, filename, dpi, cache=None):
204215
"""
205216
Read the data from the file named *filename* and convert
206217
TeX's internal units to units of *dpi* per inch.
207218
*dpi* only sets the units and does not limit the resolution.
208219
Use None to return TeX's internal units.
209220
"""
210221
_log.debug('Dvi: %s', filename)
222+
if cache is None:
223+
cache = _tex_support_cache.get_cache()
224+
self.cache = cache
211225
self.file = open(filename, 'rb')
212226
self.dpi = dpi
213227
self.fonts = {}
214228
self.state = _dvistate.pre
215229
self.baseline = self._get_baseline(filename)
230+
self.fontnames = sorted(set(self._read_fonts()))
231+
# populate kpsewhich cache with font pathnames
232+
find_tex_files([x + suffix for x in self.fontnames
233+
for suffix in ('.tfm', '.vf', '.pfb')],
234+
cache)
235+
cache.optimize()
216236

217237
def _get_baseline(self, filename):
218238
if rcParams['text.latex.preview']:
219239
base, ext = os.path.splitext(filename)
220240
baseline_filename = base + ".baseline"
221241
if os.path.exists(baseline_filename):
222242
with open(baseline_filename, 'rb') as fd:
223-
l = fd.read().split()
224-
height, depth, width = l
243+
line = fd.read().split()
244+
height, depth, width = line
225245
return float(depth)
226246
return None
227247

@@ -308,6 +328,62 @@ def _output(self):
308328
return Page(text=text, boxes=boxes, width=(maxx-minx)*d,
309329
height=(maxy_pure-miny)*d, descent=descent)
310330

331+
def _read_fonts(self):
332+
"""Read the postamble of the file and return a list of fonts used."""
333+
334+
file = self.file
335+
offset = -1
336+
while offset > -100:
337+
file.seek(offset, 2)
338+
byte = ord(file.read(1)[0])
339+
if byte != 223:
340+
break
341+
offset -= 1
342+
if offset >= -4:
343+
raise ValueError(
344+
"malformed dvi file %s: too few 223 bytes" % file.name)
345+
if byte != 2:
346+
raise ValueError(
347+
("malformed dvi file %s: post-postamble "
348+
"identification byte not 2") % file.name)
349+
file.seek(offset - 4, 2)
350+
offset = struct.unpack('!I', file.read(4))[0]
351+
file.seek(offset, 0)
352+
try:
353+
byte = ord(file.read(1)[0])
354+
except TypeError:
355+
# "ord() expected a character, but string of length 0 found"
356+
raise ValueError(
357+
"malformed dvi file %s: postamble offset %d out of range"
358+
% (file.name, offset))
359+
if byte != 248:
360+
raise ValueError(
361+
"malformed dvi file %s: postamble not found at offset %d"
362+
% (file.name, offset))
363+
364+
fonts = []
365+
file.seek(28, 1)
366+
while True:
367+
byte = ord(file.read(1)[0])
368+
if 243 <= byte <= 246:
369+
_, _, _, _, a, length = (
370+
_arg_olen1(self, byte-243),
371+
_arg(4, False, self, None),
372+
_arg(4, False, self, None),
373+
_arg(4, False, self, None),
374+
_arg(1, False, self, None),
375+
_arg(1, False, self, None))
376+
fontname = file.read(a + length)[-length:].decode('ascii')
377+
fonts.append(fontname)
378+
elif byte == 249:
379+
break
380+
else:
381+
raise ValueError(
382+
"malformed dvi file %s: opcode %d in postamble"
383+
% (file.name, byte))
384+
file.seek(0, 0)
385+
return fonts
386+
311387
def _read(self):
312388
"""
313389
Read one page from the file. Return True if successful,
@@ -616,6 +692,10 @@ class Vf(Dvi):
616692
----------
617693
618694
filename : string or bytestring
695+
vf file to read
696+
cache : _tex_support_cache instance, optional
697+
Support file cache instance, defaults to the _tex_support_cache
698+
singleton.
619699
620700
Notes
621701
-----
@@ -626,8 +706,8 @@ class Vf(Dvi):
626706
but replaces the `_read` loop and dispatch mechanism.
627707
"""
628708

629-
def __init__(self, filename):
630-
Dvi.__init__(self, filename, 0)
709+
def __init__(self, filename, cache=None):
710+
Dvi.__init__(self, filename, dpi=0, cache=cache)
631711
try:
632712
self._first_font = None
633713
self._chars = {}
@@ -638,6 +718,27 @@ def __init__(self, filename):
638718
def __getitem__(self, code):
639719
return self._chars[code]
640720

721+
def _read_fonts(self):
722+
"""Read through the font-definition section of the vf file
723+
and return the list of font names."""
724+
fonts = []
725+
self.file.seek(0, 0)
726+
while True:
727+
byte = ord(self.file.read(1)[0])
728+
if byte <= 242 or byte >= 248:
729+
break
730+
elif 243 <= byte <= 246:
731+
_ = self._arg(byte - 242)
732+
_, _, _, a, length = [self._arg(x) for x in (4, 4, 4, 1, 1)]
733+
fontname = self.file.read(a + length)[-length:].decode('ascii')
734+
fonts.append(fontname)
735+
elif byte == 247:
736+
_, k = self._arg(1), self._arg(1)
737+
_ = self.file.read(k)
738+
_, _ = self._arg(4), self._arg(4)
739+
self.file.seek(0, 0)
740+
return fonts
741+
641742
def _read(self):
642743
"""
643744
Read one page from the file. Return True if successful,
@@ -674,8 +775,8 @@ def _read(self):
674775
self._init_packet(packet_len)
675776
elif 243 <= byte <= 246:
676777
k = self._arg(byte - 242, byte == 246)
677-
c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)]
678-
self._fnt_def_real(k, c, s, d, a, l)
778+
c, s, d, a, length = [self._arg(x) for x in (4, 4, 4, 1, 1)]
779+
self._fnt_def_real(k, c, s, d, a, length)
679780
if self._first_font is None:
680781
self._first_font = k
681782
elif byte == 247: # preamble
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(FAMILY TESTING)
2+
(COMMENT Test data for matplotlib)
3+
(COMMENT Run vptovf virtual.vpl to obtain virtual.vf)
4+
(FACE O 352)
5+
(CODINGSCHEME TEX TEXT)
6+
(DESIGNSIZE R 10.0)
7+
(FONTDIMEN
8+
(SLANT R 0.0)
9+
(SPACE R 0.333334)
10+
(STRETCH R 0.166667)
11+
(SHRINK R 0.111112)
12+
(XHEIGHT R 0.430555)
13+
(QUAD R 1.000003)
14+
(EXTRASPACE R 0.111112)
15+
)
16+
(MAPFONT D 0
17+
(FONTNAME cmr10)
18+
(FONTDSIZE R 10.0)
19+
)
20+
(MAPFONT D 1
21+
(FONTNAME cmex10)
22+
(FONTDSIZE R 10.0)
23+
)

lib/matplotlib/tests/test_dviread.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ def test_dviread():
8080
assert data == correct
8181

8282

83+
@skip_if_command_unavailable(["kpsewhich", "-version"])
84+
def test_dviread_get_fonts():
85+
dir = os.path.join(os.path.dirname(__file__), 'baseline_images', 'dviread')
86+
with dr.Dvi(os.path.join(dir, 'test.dvi'), None) as dvi:
87+
assert dvi.fontnames == \
88+
['cmex10', 'cmmi10', 'cmmi5', 'cmr10', 'cmr5', 'cmr7']
89+
with dr.Vf(os.path.join(dir, 'virtual.vf')) as vf:
90+
assert vf.fontnames == ['cmex10', 'cmr10']
91+
92+
8393
def test_tex_support_cache(tmpdir):
8494
dbfile = str(tmpdir / "test.db")
8595
cache = dr._tex_support_cache(filename=dbfile)

0 commit comments

Comments
 (0)