Coverage for /usr/lib/python3/dist-packages/matplotlib/dviread.py: 28%
546 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1"""
2A module for reading dvi files output by TeX. Several limitations make
3this not (currently) useful as a general-purpose dvi preprocessor, but
4it is currently used by the pdf backend for processing usetex text.
6Interface::
8 with Dvi(filename, 72) as dvi:
9 # iterate over pages:
10 for page in dvi:
11 w, h, d = page.width, page.height, page.descent
12 for x, y, font, glyph, width in page.text:
13 fontname = font.texname
14 pointsize = font.size
15 ...
16 for x, y, height, width in page.boxes:
17 ...
18"""
20from collections import namedtuple
21import enum
22from functools import lru_cache, partial, wraps
23import logging
24import os
25from pathlib import Path
26import re
27import struct
28import subprocess
29import sys
31import numpy as np
33from matplotlib import _api, cbook
35_log = logging.getLogger(__name__)
37# Many dvi related files are looked for by external processes, require
38# additional parsing, and are used many times per rendering, which is why they
39# are cached using lru_cache().
41# Dvi is a bytecode format documented in
42# https://ctan.org/pkg/dvitype
43# https://texdoc.org/serve/dvitype.pdf/0
44#
45# The file consists of a preamble, some number of pages, a postamble,
46# and a finale. Different opcodes are allowed in different contexts,
47# so the Dvi object has a parser state:
48#
49# pre: expecting the preamble
50# outer: between pages (followed by a page or the postamble,
51# also e.g. font definitions are allowed)
52# page: processing a page
53# post_post: state after the postamble (our current implementation
54# just stops reading)
55# finale: the finale (unimplemented in our current implementation)
57_dvistate = enum.Enum('DviState', 'pre outer inpage post_post finale')
59# The marks on a page consist of text and boxes. A page also has dimensions.
60Page = namedtuple('Page', 'text boxes height width descent')
61Box = namedtuple('Box', 'x y height width')
64# Also a namedtuple, for backcompat.
65class Text(namedtuple('Text', 'x y font glyph width')):
66 """
67 A glyph in the dvi file.
69 The *x* and *y* attributes directly position the glyph. The *font*,
70 *glyph*, and *width* attributes are kept public for back-compatibility,
71 but users wanting to draw the glyph themselves are encouraged to instead
72 load the font specified by `font_path` at `font_size`, warp it with the
73 effects specified by `font_effects`, and load the glyph specified by
74 `glyph_name_or_index`.
75 """
77 def _get_pdftexmap_entry(self):
78 return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname]
80 @property
81 def font_path(self):
82 """The `~pathlib.Path` to the font for this glyph."""
83 psfont = self._get_pdftexmap_entry()
84 if psfont.filename is None:
85 raise ValueError("No usable font file found for {} ({}); "
86 "the font may lack a Type-1 version"
87 .format(psfont.psname.decode("ascii"),
88 psfont.texname.decode("ascii")))
89 return Path(psfont.filename)
91 @property
92 def font_size(self):
93 """The font size."""
94 return self.font.size
96 @property
97 def font_effects(self):
98 """
99 The "font effects" dict for this glyph.
101 This dict contains the values for this glyph of SlantFont and
102 ExtendFont (if any), read off :file:`pdftex.map`.
103 """
104 return self._get_pdftexmap_entry().effects
106 @property
107 def glyph_name_or_index(self):
108 """
109 Either the glyph name or the native charmap glyph index.
111 If :file:`pdftex.map` specifies an encoding for this glyph's font, that
112 is a mapping of glyph indices to Adobe glyph names; use it to convert
113 dvi indices to glyph names. Callers can then convert glyph names to
114 glyph indices (with FT_Get_Name_Index/get_name_index), and load the
115 glyph using FT_Load_Glyph/load_glyph.
117 If :file:`pdftex.map` specifies no encoding, the indices directly map
118 to the font's "native" charmap; glyphs should directly load using
119 FT_Load_Char/load_char after selecting the native charmap.
120 """
121 entry = self._get_pdftexmap_entry()
122 return (_parse_enc(entry.encoding)[self.glyph]
123 if entry.encoding is not None else self.glyph)
126# Opcode argument parsing
127#
128# Each of the following functions takes a Dvi object and delta,
129# which is the difference between the opcode and the minimum opcode
130# with the same meaning. Dvi opcodes often encode the number of
131# argument bytes in this delta.
133def _arg_raw(dvi, delta):
134 """Return *delta* without reading anything more from the dvi file."""
135 return delta
138def _arg(nbytes, signed, dvi, _):
139 """
140 Read *nbytes* bytes, returning the bytes interpreted as a signed integer
141 if *signed* is true, unsigned otherwise.
142 """
143 return dvi._arg(nbytes, signed)
146def _arg_slen(dvi, delta):
147 """
148 Read *delta* bytes, returning None if *delta* is zero, and the bytes
149 interpreted as a signed integer otherwise.
150 """
151 if delta == 0:
152 return None
153 return dvi._arg(delta, True)
156def _arg_slen1(dvi, delta):
157 """
158 Read *delta*+1 bytes, returning the bytes interpreted as signed.
159 """
160 return dvi._arg(delta + 1, True)
163def _arg_ulen1(dvi, delta):
164 """
165 Read *delta*+1 bytes, returning the bytes interpreted as unsigned.
166 """
167 return dvi._arg(delta + 1, False)
170def _arg_olen1(dvi, delta):
171 """
172 Read *delta*+1 bytes, returning the bytes interpreted as
173 unsigned integer for 0<=*delta*<3 and signed if *delta*==3.
174 """
175 return dvi._arg(delta + 1, delta == 3)
178_arg_mapping = dict(raw=_arg_raw,
179 u1=partial(_arg, 1, False),
180 u4=partial(_arg, 4, False),
181 s4=partial(_arg, 4, True),
182 slen=_arg_slen,
183 olen1=_arg_olen1,
184 slen1=_arg_slen1,
185 ulen1=_arg_ulen1)
188def _dispatch(table, min, max=None, state=None, args=('raw',)):
189 """
190 Decorator for dispatch by opcode. Sets the values in *table*
191 from *min* to *max* to this method, adds a check that the Dvi state
192 matches *state* if not None, reads arguments from the file according
193 to *args*.
195 Parameters
196 ----------
197 table : dict[int, callable]
198 The dispatch table to be filled in.
200 min, max : int
201 Range of opcodes that calls the registered function; *max* defaults to
202 *min*.
204 state : _dvistate, optional
205 State of the Dvi object in which these opcodes are allowed.
207 args : list[str], default: ['raw']
208 Sequence of argument specifications:
210 - 'raw': opcode minus minimum
211 - 'u1': read one unsigned byte
212 - 'u4': read four bytes, treat as an unsigned number
213 - 's4': read four bytes, treat as a signed number
214 - 'slen': read (opcode - minimum) bytes, treat as signed
215 - 'slen1': read (opcode - minimum + 1) bytes, treat as signed
216 - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned
217 - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned
218 if under four bytes, signed if four bytes
219 """
220 def decorate(method):
221 get_args = [_arg_mapping[x] for x in args]
223 @wraps(method)
224 def wrapper(self, byte):
225 if state is not None and self.state != state:
226 raise ValueError("state precondition failed")
227 return method(self, *[f(self, byte-min) for f in get_args])
228 if max is None:
229 table[min] = wrapper
230 else:
231 for i in range(min, max+1):
232 assert table[i] is None
233 table[i] = wrapper
234 return wrapper
235 return decorate
238class Dvi:
239 """
240 A reader for a dvi ("device-independent") file, as produced by TeX.
242 The current implementation can only iterate through pages in order,
243 and does not even attempt to verify the postamble.
245 This class can be used as a context manager to close the underlying
246 file upon exit. Pages can be read via iteration. Here is an overly
247 simple way to extract text without trying to detect whitespace::
249 >>> with matplotlib.dviread.Dvi('input.dvi', 72) as dvi:
250 ... for page in dvi:
251 ... print(''.join(chr(t.glyph) for t in page.text))
252 """
253 # dispatch table
254 _dtable = [None] * 256
255 _dispatch = partial(_dispatch, _dtable)
257 def __init__(self, filename, dpi):
258 """
259 Read the data from the file named *filename* and convert
260 TeX's internal units to units of *dpi* per inch.
261 *dpi* only sets the units and does not limit the resolution.
262 Use None to return TeX's internal units.
263 """
264 _log.debug('Dvi: %s', filename)
265 self.file = open(filename, 'rb')
266 self.dpi = dpi
267 self.fonts = {}
268 self.state = _dvistate.pre
270 baseline = _api.deprecated("3.5")(property(lambda self: None))
272 def __enter__(self):
273 """Context manager enter method, does nothing."""
274 return self
276 def __exit__(self, etype, evalue, etrace):
277 """
278 Context manager exit method, closes the underlying file if it is open.
279 """
280 self.close()
282 def __iter__(self):
283 """
284 Iterate through the pages of the file.
286 Yields
287 ------
288 Page
289 Details of all the text and box objects on the page.
290 The Page tuple contains lists of Text and Box tuples and
291 the page dimensions, and the Text and Box tuples contain
292 coordinates transformed into a standard Cartesian
293 coordinate system at the dpi value given when initializing.
294 The coordinates are floating point numbers, but otherwise
295 precision is not lost and coordinate values are not clipped to
296 integers.
297 """
298 while self._read():
299 yield self._output()
301 def close(self):
302 """Close the underlying file if it is open."""
303 if not self.file.closed:
304 self.file.close()
306 def _output(self):
307 """
308 Output the text and boxes belonging to the most recent page.
309 page = dvi._output()
310 """
311 minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf
312 maxy_pure = -np.inf
313 for elt in self.text + self.boxes:
314 if isinstance(elt, Box):
315 x, y, h, w = elt
316 e = 0 # zero depth
317 else: # glyph
318 x, y, font, g, w = elt
319 h, e = font._height_depth_of(g)
320 minx = min(minx, x)
321 miny = min(miny, y - h)
322 maxx = max(maxx, x + w)
323 maxy = max(maxy, y + e)
324 maxy_pure = max(maxy_pure, y)
325 if self._baseline_v is not None:
326 maxy_pure = self._baseline_v # This should normally be the case.
327 self._baseline_v = None
329 if not self.text and not self.boxes: # Avoid infs/nans from inf+/-inf.
330 return Page(text=[], boxes=[], width=0, height=0, descent=0)
332 if self.dpi is None:
333 # special case for ease of debugging: output raw dvi coordinates
334 return Page(text=self.text, boxes=self.boxes,
335 width=maxx-minx, height=maxy_pure-miny,
336 descent=maxy-maxy_pure)
338 # convert from TeX's "scaled points" to dpi units
339 d = self.dpi / (72.27 * 2**16)
340 descent = (maxy - maxy_pure) * d
342 text = [Text((x-minx)*d, (maxy-y)*d - descent, f, g, w*d)
343 for (x, y, f, g, w) in self.text]
344 boxes = [Box((x-minx)*d, (maxy-y)*d - descent, h*d, w*d)
345 for (x, y, h, w) in self.boxes]
347 return Page(text=text, boxes=boxes, width=(maxx-minx)*d,
348 height=(maxy_pure-miny)*d, descent=descent)
350 def _read(self):
351 """
352 Read one page from the file. Return True if successful,
353 False if there were no more pages.
354 """
355 # Pages appear to start with the sequence
356 # bop (begin of page)
357 # xxx comment
358 # <push, ..., pop> # if using chemformula
359 # down
360 # push
361 # down
362 # <push, push, xxx, right, xxx, pop, pop> # if using xcolor
363 # down
364 # push
365 # down (possibly multiple)
366 # push <= here, v is the baseline position.
367 # etc.
368 # (dviasm is useful to explore this structure.)
369 # Thus, we use the vertical position at the first time the stack depth
370 # reaches 3, while at least three "downs" have been executed (excluding
371 # those popped out (corresponding to the chemformula preamble)), as the
372 # baseline (the "down" count is necessary to handle xcolor).
373 down_stack = [0]
374 self._baseline_v = None
375 while True:
376 byte = self.file.read(1)[0]
377 self._dtable[byte](self, byte)
378 name = self._dtable[byte].__name__
379 if name == "_push":
380 down_stack.append(down_stack[-1])
381 elif name == "_pop":
382 down_stack.pop()
383 elif name == "_down":
384 down_stack[-1] += 1
385 if (self._baseline_v is None
386 and len(getattr(self, "stack", [])) == 3
387 and down_stack[-1] >= 4):
388 self._baseline_v = self.v
389 if byte == 140: # end of page
390 return True
391 if self.state is _dvistate.post_post: # end of file
392 self.close()
393 return False
395 def _arg(self, nbytes, signed=False):
396 """
397 Read and return an integer argument *nbytes* long.
398 Signedness is determined by the *signed* keyword.
399 """
400 buf = self.file.read(nbytes)
401 value = buf[0]
402 if signed and value >= 0x80:
403 value = value - 0x100
404 for b in buf[1:]:
405 value = 0x100*value + b
406 return value
408 @_dispatch(min=0, max=127, state=_dvistate.inpage)
409 def _set_char_immediate(self, char):
410 self._put_char_real(char)
411 self.h += self.fonts[self.f]._width_of(char)
413 @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',))
414 def _set_char(self, char):
415 self._put_char_real(char)
416 self.h += self.fonts[self.f]._width_of(char)
418 @_dispatch(132, state=_dvistate.inpage, args=('s4', 's4'))
419 def _set_rule(self, a, b):
420 self._put_rule_real(a, b)
421 self.h += b
423 @_dispatch(min=133, max=136, state=_dvistate.inpage, args=('olen1',))
424 def _put_char(self, char):
425 self._put_char_real(char)
427 def _put_char_real(self, char):
428 font = self.fonts[self.f]
429 if font._vf is None:
430 self.text.append(Text(self.h, self.v, font, char,
431 font._width_of(char)))
432 else:
433 scale = font._scale
434 for x, y, f, g, w in font._vf[char].text:
435 newf = DviFont(scale=_mul2012(scale, f._scale),
436 tfm=f._tfm, texname=f.texname, vf=f._vf)
437 self.text.append(Text(self.h + _mul2012(x, scale),
438 self.v + _mul2012(y, scale),
439 newf, g, newf._width_of(g)))
440 self.boxes.extend([Box(self.h + _mul2012(x, scale),
441 self.v + _mul2012(y, scale),
442 _mul2012(a, scale), _mul2012(b, scale))
443 for x, y, a, b in font._vf[char].boxes])
445 @_dispatch(137, state=_dvistate.inpage, args=('s4', 's4'))
446 def _put_rule(self, a, b):
447 self._put_rule_real(a, b)
449 def _put_rule_real(self, a, b):
450 if a > 0 and b > 0:
451 self.boxes.append(Box(self.h, self.v, a, b))
453 @_dispatch(138)
454 def _nop(self, _):
455 pass
457 @_dispatch(139, state=_dvistate.outer, args=('s4',)*11)
458 def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p):
459 self.state = _dvistate.inpage
460 self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0
461 self.stack = []
462 self.text = [] # list of Text objects
463 self.boxes = [] # list of Box objects
465 @_dispatch(140, state=_dvistate.inpage)
466 def _eop(self, _):
467 self.state = _dvistate.outer
468 del self.h, self.v, self.w, self.x, self.y, self.z, self.stack
470 @_dispatch(141, state=_dvistate.inpage)
471 def _push(self, _):
472 self.stack.append((self.h, self.v, self.w, self.x, self.y, self.z))
474 @_dispatch(142, state=_dvistate.inpage)
475 def _pop(self, _):
476 self.h, self.v, self.w, self.x, self.y, self.z = self.stack.pop()
478 @_dispatch(min=143, max=146, state=_dvistate.inpage, args=('slen1',))
479 def _right(self, b):
480 self.h += b
482 @_dispatch(min=147, max=151, state=_dvistate.inpage, args=('slen',))
483 def _right_w(self, new_w):
484 if new_w is not None:
485 self.w = new_w
486 self.h += self.w
488 @_dispatch(min=152, max=156, state=_dvistate.inpage, args=('slen',))
489 def _right_x(self, new_x):
490 if new_x is not None:
491 self.x = new_x
492 self.h += self.x
494 @_dispatch(min=157, max=160, state=_dvistate.inpage, args=('slen1',))
495 def _down(self, a):
496 self.v += a
498 @_dispatch(min=161, max=165, state=_dvistate.inpage, args=('slen',))
499 def _down_y(self, new_y):
500 if new_y is not None:
501 self.y = new_y
502 self.v += self.y
504 @_dispatch(min=166, max=170, state=_dvistate.inpage, args=('slen',))
505 def _down_z(self, new_z):
506 if new_z is not None:
507 self.z = new_z
508 self.v += self.z
510 @_dispatch(min=171, max=234, state=_dvistate.inpage)
511 def _fnt_num_immediate(self, k):
512 self.f = k
514 @_dispatch(min=235, max=238, state=_dvistate.inpage, args=('olen1',))
515 def _fnt_num(self, new_f):
516 self.f = new_f
518 @_dispatch(min=239, max=242, args=('ulen1',))
519 def _xxx(self, datalen):
520 special = self.file.read(datalen)
521 _log.debug(
522 'Dvi._xxx: encountered special: %s',
523 ''.join([chr(ch) if 32 <= ch < 127 else '<%02x>' % ch
524 for ch in special]))
526 @_dispatch(min=243, max=246, args=('olen1', 'u4', 'u4', 'u4', 'u1', 'u1'))
527 def _fnt_def(self, k, c, s, d, a, l):
528 self._fnt_def_real(k, c, s, d, a, l)
530 def _fnt_def_real(self, k, c, s, d, a, l):
531 n = self.file.read(a + l)
532 fontname = n[-l:].decode('ascii')
533 tfm = _tfmfile(fontname)
534 if c != 0 and tfm.checksum != 0 and c != tfm.checksum:
535 raise ValueError('tfm checksum mismatch: %s' % n)
536 try:
537 vf = _vffile(fontname)
538 except FileNotFoundError:
539 vf = None
540 self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf)
542 @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1'))
543 def _pre(self, i, num, den, mag, k):
544 self.file.read(k) # comment in the dvi file
545 if i != 2:
546 raise ValueError("Unknown dvi format %d" % i)
547 if num != 25400000 or den != 7227 * 2**16:
548 raise ValueError("Nonstandard units in dvi file")
549 # meaning: TeX always uses those exact values, so it
550 # should be enough for us to support those
551 # (There are 72.27 pt to an inch so 7227 pt =
552 # 7227 * 2**16 sp to 100 in. The numerator is multiplied
553 # by 10^5 to get units of 10**-7 meters.)
554 if mag != 1000:
555 raise ValueError("Nonstandard magnification in dvi file")
556 # meaning: LaTeX seems to frown on setting \mag, so
557 # I think we can assume this is constant
558 self.state = _dvistate.outer
560 @_dispatch(248, state=_dvistate.outer)
561 def _post(self, _):
562 self.state = _dvistate.post_post
563 # TODO: actually read the postamble and finale?
564 # currently post_post just triggers closing the file
566 @_dispatch(249)
567 def _post_post(self, _):
568 raise NotImplementedError
570 @_dispatch(min=250, max=255)
571 def _malformed(self, offset):
572 raise ValueError(f"unknown command: byte {250 + offset}")
575class DviFont:
576 """
577 Encapsulation of a font that a DVI file can refer to.
579 This class holds a font's texname and size, supports comparison,
580 and knows the widths of glyphs in the same units as the AFM file.
581 There are also internal attributes (for use by dviread.py) that
582 are *not* used for comparison.
584 The size is in Adobe points (converted from TeX points).
586 Parameters
587 ----------
588 scale : float
589 Factor by which the font is scaled from its natural size.
590 tfm : Tfm
591 TeX font metrics for this font
592 texname : bytes
593 Name of the font as used internally by TeX and friends, as an ASCII
594 bytestring. This is usually very different from any external font
595 names; `PsfontsMap` can be used to find the external name of the font.
596 vf : Vf
597 A TeX "virtual font" file, or None if this font is not virtual.
599 Attributes
600 ----------
601 texname : bytes
602 size : float
603 Size of the font in Adobe points, converted from the slightly
604 smaller TeX points.
605 widths : list
606 Widths of glyphs in glyph-space units, typically 1/1000ths of
607 the point size.
609 """
610 __slots__ = ('texname', 'size', 'widths', '_scale', '_vf', '_tfm')
612 def __init__(self, scale, tfm, texname, vf):
613 _api.check_isinstance(bytes, texname=texname)
614 self._scale = scale
615 self._tfm = tfm
616 self.texname = texname
617 self._vf = vf
618 self.size = scale * (72.0 / (72.27 * 2**16))
619 try:
620 nchars = max(tfm.width) + 1
621 except ValueError:
622 nchars = 0
623 self.widths = [(1000*tfm.width.get(char, 0)) >> 20
624 for char in range(nchars)]
626 def __eq__(self, other):
627 return (type(self) == type(other)
628 and self.texname == other.texname and self.size == other.size)
630 def __ne__(self, other):
631 return not self.__eq__(other)
633 def __repr__(self):
634 return "<{}: {}>".format(type(self).__name__, self.texname)
636 def _width_of(self, char):
637 """Width of char in dvi units."""
638 width = self._tfm.width.get(char, None)
639 if width is not None:
640 return _mul2012(width, self._scale)
641 _log.debug('No width for char %d in font %s.', char, self.texname)
642 return 0
644 def _height_depth_of(self, char):
645 """Height and depth of char in dvi units."""
646 result = []
647 for metric, name in ((self._tfm.height, "height"),
648 (self._tfm.depth, "depth")):
649 value = metric.get(char, None)
650 if value is None:
651 _log.debug('No %s for char %d in font %s',
652 name, char, self.texname)
653 result.append(0)
654 else:
655 result.append(_mul2012(value, self._scale))
656 # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent
657 # so that TeX aligns equations properly
658 # (https://tex.stackexchange.com/q/526103/)
659 # but we actually care about the rasterization depth to align
660 # the dvipng-generated images.
661 if re.match(br'^cmsy\d+$', self.texname) and char == 0:
662 result[-1] = 0
663 return result
666class Vf(Dvi):
667 r"""
668 A virtual font (\*.vf file) containing subroutines for dvi files.
670 Parameters
671 ----------
672 filename : str or path-like
674 Notes
675 -----
676 The virtual font format is a derivative of dvi:
677 http://mirrors.ctan.org/info/knuth/virtual-fonts
678 This class reuses some of the machinery of `Dvi`
679 but replaces the `_read` loop and dispatch mechanism.
681 Examples
682 --------
683 ::
685 vf = Vf(filename)
686 glyph = vf[code]
687 glyph.text, glyph.boxes, glyph.width
688 """
690 def __init__(self, filename):
691 super().__init__(filename, 0)
692 try:
693 self._first_font = None
694 self._chars = {}
695 self._read()
696 finally:
697 self.close()
699 def __getitem__(self, code):
700 return self._chars[code]
702 def _read(self):
703 """
704 Read one page from the file. Return True if successful,
705 False if there were no more pages.
706 """
707 packet_char, packet_ends = None, None
708 packet_len, packet_width = None, None
709 while True:
710 byte = self.file.read(1)[0]
711 # If we are in a packet, execute the dvi instructions
712 if self.state is _dvistate.inpage:
713 byte_at = self.file.tell()-1
714 if byte_at == packet_ends:
715 self._finalize_packet(packet_char, packet_width)
716 packet_len, packet_char, packet_width = None, None, None
717 # fall through to out-of-packet code
718 elif byte_at > packet_ends:
719 raise ValueError("Packet length mismatch in vf file")
720 else:
721 if byte in (139, 140) or byte >= 243:
722 raise ValueError(
723 "Inappropriate opcode %d in vf file" % byte)
724 Dvi._dtable[byte](self, byte)
725 continue
727 # We are outside a packet
728 if byte < 242: # a short packet (length given by byte)
729 packet_len = byte
730 packet_char, packet_width = self._arg(1), self._arg(3)
731 packet_ends = self._init_packet(byte)
732 self.state = _dvistate.inpage
733 elif byte == 242: # a long packet
734 packet_len, packet_char, packet_width = \
735 [self._arg(x) for x in (4, 4, 4)]
736 self._init_packet(packet_len)
737 elif 243 <= byte <= 246:
738 k = self._arg(byte - 242, byte == 246)
739 c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)]
740 self._fnt_def_real(k, c, s, d, a, l)
741 if self._first_font is None:
742 self._first_font = k
743 elif byte == 247: # preamble
744 i, k = self._arg(1), self._arg(1)
745 x = self.file.read(k)
746 cs, ds = self._arg(4), self._arg(4)
747 self._pre(i, x, cs, ds)
748 elif byte == 248: # postamble (just some number of 248s)
749 break
750 else:
751 raise ValueError("Unknown vf opcode %d" % byte)
753 def _init_packet(self, pl):
754 if self.state != _dvistate.outer:
755 raise ValueError("Misplaced packet in vf file")
756 self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0
757 self.stack, self.text, self.boxes = [], [], []
758 self.f = self._first_font
759 return self.file.tell() + pl
761 def _finalize_packet(self, packet_char, packet_width):
762 self._chars[packet_char] = Page(
763 text=self.text, boxes=self.boxes, width=packet_width,
764 height=None, descent=None)
765 self.state = _dvistate.outer
767 def _pre(self, i, x, cs, ds):
768 if self.state is not _dvistate.pre:
769 raise ValueError("pre command in middle of vf file")
770 if i != 202:
771 raise ValueError("Unknown vf format %d" % i)
772 if len(x):
773 _log.debug('vf file comment: %s', x)
774 self.state = _dvistate.outer
775 # cs = checksum, ds = design size
778def _mul2012(num1, num2):
779 """Multiply two numbers in 20.12 fixed point format."""
780 # Separated into a function because >> has surprising precedence
781 return (num1*num2) >> 20
784class Tfm:
785 """
786 A TeX Font Metric file.
788 This implementation covers only the bare minimum needed by the Dvi class.
790 Parameters
791 ----------
792 filename : str or path-like
794 Attributes
795 ----------
796 checksum : int
797 Used for verifying against the dvi file.
798 design_size : int
799 Design size of the font (unknown units)
800 width, height, depth : dict
801 Dimensions of each character, need to be scaled by the factor
802 specified in the dvi file. These are dicts because indexing may
803 not start from 0.
804 """
805 __slots__ = ('checksum', 'design_size', 'width', 'height', 'depth')
807 def __init__(self, filename):
808 _log.debug('opening tfm file %s', filename)
809 with open(filename, 'rb') as file:
810 header1 = file.read(24)
811 lh, bc, ec, nw, nh, nd = struct.unpack('!6H', header1[2:14])
812 _log.debug('lh=%d, bc=%d, ec=%d, nw=%d, nh=%d, nd=%d',
813 lh, bc, ec, nw, nh, nd)
814 header2 = file.read(4*lh)
815 self.checksum, self.design_size = struct.unpack('!2I', header2[:8])
816 # there is also encoding information etc.
817 char_info = file.read(4*(ec-bc+1))
818 widths = struct.unpack(f'!{nw}i', file.read(4*nw))
819 heights = struct.unpack(f'!{nh}i', file.read(4*nh))
820 depths = struct.unpack(f'!{nd}i', file.read(4*nd))
821 self.width, self.height, self.depth = {}, {}, {}
822 for idx, char in enumerate(range(bc, ec+1)):
823 byte0 = char_info[4*idx]
824 byte1 = char_info[4*idx+1]
825 self.width[char] = widths[byte0]
826 self.height[char] = heights[byte1 >> 4]
827 self.depth[char] = depths[byte1 & 0xf]
830PsFont = namedtuple('PsFont', 'texname psname effects encoding filename')
833class PsfontsMap:
834 """
835 A psfonts.map formatted file, mapping TeX fonts to PS fonts.
837 Parameters
838 ----------
839 filename : str or path-like
841 Notes
842 -----
843 For historical reasons, TeX knows many Type-1 fonts by different
844 names than the outside world. (For one thing, the names have to
845 fit in eight characters.) Also, TeX's native fonts are not Type-1
846 but Metafont, which is nontrivial to convert to PostScript except
847 as a bitmap. While high-quality conversions to Type-1 format exist
848 and are shipped with modern TeX distributions, we need to know
849 which Type-1 fonts are the counterparts of which native fonts. For
850 these reasons a mapping is needed from internal font names to font
851 file names.
853 A texmf tree typically includes mapping files called e.g.
854 :file:`psfonts.map`, :file:`pdftex.map`, or :file:`dvipdfm.map`.
855 The file :file:`psfonts.map` is used by :program:`dvips`,
856 :file:`pdftex.map` by :program:`pdfTeX`, and :file:`dvipdfm.map`
857 by :program:`dvipdfm`. :file:`psfonts.map` might avoid embedding
858 the 35 PostScript fonts (i.e., have no filename for them, as in
859 the Times-Bold example above), while the pdf-related files perhaps
860 only avoid the "Base 14" pdf fonts. But the user may have
861 configured these files differently.
863 Examples
864 --------
865 >>> map = PsfontsMap(find_tex_file('pdftex.map'))
866 >>> entry = map[b'ptmbo8r']
867 >>> entry.texname
868 b'ptmbo8r'
869 >>> entry.psname
870 b'Times-Bold'
871 >>> entry.encoding
872 '/usr/local/texlive/2008/texmf-dist/fonts/enc/dvips/base/8r.enc'
873 >>> entry.effects
874 {'slant': 0.16700000000000001}
875 >>> entry.filename
876 """
877 __slots__ = ('_filename', '_unparsed', '_parsed')
879 # Create a filename -> PsfontsMap cache, so that calling
880 # `PsfontsMap(filename)` with the same filename a second time immediately
881 # returns the same object.
882 @lru_cache()
883 def __new__(cls, filename):
884 self = object.__new__(cls)
885 self._filename = os.fsdecode(filename)
886 # Some TeX distributions have enormous pdftex.map files which would
887 # take hundreds of milliseconds to parse, but it is easy enough to just
888 # store the unparsed lines (keyed by the first word, which is the
889 # texname) and parse them on-demand.
890 with open(filename, 'rb') as file:
891 self._unparsed = {}
892 for line in file:
893 tfmname = line.split(b' ', 1)[0]
894 self._unparsed.setdefault(tfmname, []).append(line)
895 self._parsed = {}
896 return self
898 def __getitem__(self, texname):
899 assert isinstance(texname, bytes)
900 if texname in self._unparsed:
901 for line in self._unparsed.pop(texname):
902 if self._parse_and_cache_line(line):
903 break
904 try:
905 return self._parsed[texname]
906 except KeyError:
907 raise LookupError(
908 f"An associated PostScript font (required by Matplotlib) "
909 f"could not be found for TeX font {texname.decode('ascii')!r} "
910 f"in {self._filename!r}; this problem can often be solved by "
911 f"installing a suitable PostScript font package in your TeX "
912 f"package manager") from None
914 def _parse_and_cache_line(self, line):
915 """
916 Parse a line in the font mapping file.
918 The format is (partially) documented at
919 http://mirrors.ctan.org/systems/doc/pdftex/manual/pdftex-a.pdf
920 https://tug.org/texinfohtml/dvips.html#psfonts_002emap
921 Each line can have the following fields:
923 - tfmname (first, only required field),
924 - psname (defaults to tfmname, must come immediately after tfmname if
925 present),
926 - fontflags (integer, must come immediately after psname if present,
927 ignored by us),
928 - special (SlantFont and ExtendFont, only field that is double-quoted),
929 - fontfile, encodingfile (optional, prefixed by <, <<, or <[; << always
930 precedes a font, <[ always precedes an encoding, < can precede either
931 but then an encoding file must have extension .enc; < and << also
932 request different font subsetting behaviors but we ignore that; < can
933 be separated from the filename by whitespace).
935 special, fontfile, and encodingfile can appear in any order.
936 """
937 # If the map file specifies multiple encodings for a font, we
938 # follow pdfTeX in choosing the last one specified. Such
939 # entries are probably mistakes but they have occurred.
940 # https://tex.stackexchange.com/q/10826/
942 if not line or line.startswith((b" ", b"%", b"*", b";", b"#")):
943 return
944 tfmname = basename = special = encodingfile = fontfile = None
945 is_subsetted = is_t1 = is_truetype = False
946 matches = re.finditer(br'"([^"]*)(?:"|$)|(\S+)', line)
947 for match in matches:
948 quoted, unquoted = match.groups()
949 if unquoted:
950 if unquoted.startswith(b"<<"): # font
951 fontfile = unquoted[2:]
952 elif unquoted.startswith(b"<["): # encoding
953 encodingfile = unquoted[2:]
954 elif unquoted.startswith(b"<"): # font or encoding
955 word = (
956 # <foo => foo
957 unquoted[1:]
958 # < by itself => read the next word
959 or next(filter(None, next(matches).groups())))
960 if word.endswith(b".enc"):
961 encodingfile = word
962 else:
963 fontfile = word
964 is_subsetted = True
965 elif tfmname is None:
966 tfmname = unquoted
967 elif basename is None:
968 basename = unquoted
969 elif quoted:
970 special = quoted
971 effects = {}
972 if special:
973 words = reversed(special.split())
974 for word in words:
975 if word == b"SlantFont":
976 effects["slant"] = float(next(words))
977 elif word == b"ExtendFont":
978 effects["extend"] = float(next(words))
980 # Verify some properties of the line that would cause it to be ignored
981 # otherwise.
982 if fontfile is not None:
983 if fontfile.endswith((b".ttf", b".ttc")):
984 is_truetype = True
985 elif not fontfile.endswith(b".otf"):
986 is_t1 = True
987 elif basename is not None:
988 is_t1 = True
989 if is_truetype and is_subsetted and encodingfile is None:
990 return
991 if not is_t1 and ("slant" in effects or "extend" in effects):
992 return
993 if abs(effects.get("slant", 0)) > 1:
994 return
995 if abs(effects.get("extend", 0)) > 2:
996 return
998 if basename is None:
999 basename = tfmname
1000 if encodingfile is not None:
1001 encodingfile = _find_tex_file(encodingfile)
1002 if fontfile is not None:
1003 fontfile = _find_tex_file(fontfile)
1004 self._parsed[tfmname] = PsFont(
1005 texname=tfmname, psname=basename, effects=effects,
1006 encoding=encodingfile, filename=fontfile)
1007 return True
1010def _parse_enc(path):
1011 r"""
1012 Parse a \*.enc file referenced from a psfonts.map style file.
1014 The format supported by this function is a tiny subset of PostScript.
1016 Parameters
1017 ----------
1018 path : `os.PathLike`
1020 Returns
1021 -------
1022 list
1023 The nth entry of the list is the PostScript glyph name of the nth
1024 glyph.
1025 """
1026 no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii"))
1027 array = re.search(r"(?s)\[(.*)\]", no_comments).group(1)
1028 lines = [line for line in array.split() if line]
1029 if all(line.startswith("/") for line in lines):
1030 return [line[1:] for line in lines]
1031 else:
1032 raise ValueError(
1033 "Failed to parse {} as Postscript encoding".format(path))
1036class _LuatexKpsewhich:
1037 @lru_cache() # A singleton.
1038 def __new__(cls):
1039 self = object.__new__(cls)
1040 self._proc = self._new_proc()
1041 return self
1043 def _new_proc(self):
1044 return subprocess.Popen(
1045 ["luatex", "--luaonly",
1046 str(cbook._get_data_path("kpsewhich.lua"))],
1047 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1049 def search(self, filename):
1050 if self._proc.poll() is not None: # Dead, restart it.
1051 self._proc = self._new_proc()
1052 self._proc.stdin.write(os.fsencode(filename) + b"\n")
1053 self._proc.stdin.flush()
1054 out = self._proc.stdout.readline().rstrip()
1055 return None if out == b"nil" else os.fsdecode(out)
1058@lru_cache()
1059@_api.delete_parameter("3.5", "format")
1060def _find_tex_file(filename, format=None):
1061 """
1062 Find a file in the texmf tree using kpathsea_.
1064 The kpathsea library, provided by most existing TeX distributions, both
1065 on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived
1066 luatex process if luatex is installed, or via kpsewhich otherwise.
1068 .. _kpathsea: https://www.tug.org/kpathsea/
1070 Parameters
1071 ----------
1072 filename : str or path-like
1073 format : str or bytes
1074 Used as the value of the ``--format`` option to :program:`kpsewhich`.
1075 Could be e.g. 'tfm' or 'vf' to limit the search to that type of files.
1076 Deprecated.
1078 Raises
1079 ------
1080 FileNotFoundError
1081 If the file is not found.
1082 """
1084 # we expect these to always be ascii encoded, but use utf-8
1085 # out of caution
1086 if isinstance(filename, bytes):
1087 filename = filename.decode('utf-8', errors='replace')
1088 if isinstance(format, bytes):
1089 format = format.decode('utf-8', errors='replace')
1091 try:
1092 lk = _LuatexKpsewhich()
1093 except FileNotFoundError:
1094 lk = None # Fallback to directly calling kpsewhich, as below.
1096 if lk and format is None:
1097 path = lk.search(filename)
1099 else:
1100 if os.name == 'nt':
1101 # On Windows only, kpathsea can use utf-8 for cmd args and output.
1102 # The `command_line_encoding` environment variable is set to force
1103 # it to always use utf-8 encoding. See Matplotlib issue #11848.
1104 kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'},
1105 'encoding': 'utf-8'}
1106 else: # On POSIX, run through the equivalent of os.fsdecode().
1107 kwargs = {'encoding': sys.getfilesystemencoding(),
1108 'errors': 'surrogateescape'}
1110 cmd = ['kpsewhich']
1111 if format is not None:
1112 cmd += ['--format=' + format]
1113 cmd += [filename]
1114 try:
1115 path = (cbook._check_and_log_subprocess(cmd, _log, **kwargs)
1116 .rstrip('\n'))
1117 except (FileNotFoundError, RuntimeError):
1118 path = None
1120 if path:
1121 return path
1122 else:
1123 raise FileNotFoundError(
1124 f"Matplotlib's TeX implementation searched for a file named "
1125 f"{filename!r} in your texmf tree, but could not find it")
1128# After the deprecation period elapses, delete this shim and rename
1129# _find_tex_file to find_tex_file everywhere.
1130@_api.delete_parameter("3.5", "format")
1131def find_tex_file(filename, format=None):
1132 try:
1133 return (_find_tex_file(filename, format) if format is not None else
1134 _find_tex_file(filename))
1135 except FileNotFoundError as exc:
1136 _api.warn_deprecated(
1137 "3.6", message=f"{exc.args[0]}; in the future, this will raise a "
1138 f"FileNotFoundError.")
1139 return ""
1142find_tex_file.__doc__ = _find_tex_file.__doc__
1145@lru_cache()
1146def _fontfile(cls, suffix, texname):
1147 return cls(_find_tex_file(texname + suffix))
1150_tfmfile = partial(_fontfile, Tfm, ".tfm")
1151_vffile = partial(_fontfile, Vf, ".vf")
1154if __name__ == '__main__':
1155 from argparse import ArgumentParser
1156 import itertools
1158 parser = ArgumentParser()
1159 parser.add_argument("filename")
1160 parser.add_argument("dpi", nargs="?", type=float, default=None)
1161 args = parser.parse_args()
1162 with Dvi(args.filename, args.dpi) as dvi:
1163 fontmap = PsfontsMap(_find_tex_file('pdftex.map'))
1164 for page in dvi:
1165 print(f"=== new page === "
1166 f"(w: {page.width}, h: {page.height}, d: {page.descent})")
1167 for font, group in itertools.groupby(
1168 page.text, lambda text: text.font):
1169 print(f"font: {font.texname.decode('latin-1')!r}\t"
1170 f"scale: {font._scale / 2 ** 20}")
1171 print("x", "y", "glyph", "chr", "w", "(glyphs)", sep="\t")
1172 for text in group:
1173 print(text.x, text.y, text.glyph,
1174 chr(text.glyph) if chr(text.glyph).isprintable()
1175 else ".",
1176 text.width, sep="\t")
1177 if page.boxes:
1178 print("x", "y", "h", "w", "", "(boxes)", sep="\t")
1179 for box in page.boxes:
1180 print(box.x, box.y, box.height, box.width, sep="\t")