Matplotlib dviread
| Language | Python |
|---|---|
| License | Matplotlib license (PSF-based, BSD-compatible) |
| Repository | github.com/matplotlib/matplotlib (lib/matplotlib/dviread.py) |
| Documentation | matplotlib.dviread |
| Depends on | a TeX installation (kpsewhich and TFM font metrics) to interpret pages |
| Read / write | yes / no |
matplotlib.dviread is the DVI reader Matplotlib uses for text.usetex: Matplotlib runs LaTeX, reads the DVI with this module and draws each glyph itself. It’s a public module and works on any DVI file, including XeTeX and LuaTeX output with native fonts in recent versions.
Dvi(filename, dpi) iterates over pages. Each page is a Page named tuple with text (glyphs), boxes (rules), and width, height and descent of the area covered by ink (not the paper size). Coordinates are converted to units of dpi per inch; pass None to keep TeX’s internal units. Pages can only be read in order.
To resolve fonts it calls kpsewhich to find TFM files, so a TeX distribution (TeX Live or MiKTeX) must be installed. It doesn’t render glyphs by itself.
Installation#
pip install matplotlib
Install TeX Live or MiKTeX, and make sure kpsewhich is on PATH.
Basic usage: list pages, glyphs and rules#
from matplotlib.dviread import Dvi
with Dvi("paper.dvi", 72) as dvi: # coordinates in points (72 per inch)
for n, page in enumerate(dvi, 1):
print(f"page {n}: content {page.width:.1f} x {page.height:.1f} pt, "
f"{len(page.text)} glyphs, {len(page.boxes)} rules")
for t in page.text[:5]:
print(f" glyph {t.glyph} of {t.font.texname.decode()} "
f"at ({t.x:.1f}, {t.y:.1f}), {t.font.size:.1f} pt")
for b in page.boxes[:3]:
print(f" rule at ({b.x:.1f}, {b.y:.1f}), {b.width:.1f} x {b.height:.1f}")
How to extract approximate text from a DVI file#
DVI stores font character codes, not text. For Computer Modern text fonts, codes 32-126 are mostly ASCII, so grouping glyphs by baseline gives readable text:
from itertools import groupby
from matplotlib.dviread import Dvi
with Dvi("paper.dvi", 72) as dvi:
for page in dvi:
glyphs = sorted(page.text, key=lambda t: (-round(t.y), t.x)) # y goes up
for _, line in groupby(glyphs, key=lambda t: -round(t.y)):
out, last_end = "", None
for t in line:
if last_end is not None and t.x - last_end > 2: # gap = space
out += " "
out += chr(t.glyph) if 32 <= t.glyph < 127 else "?"
last_end = t.x + t.width # width is in the same units as x
print(out)
print("\f")
Ligatures (fi is code 12 in cmr10), accents and math symbols need a per-font encoding table for real text extraction.
How to list the fonts a DVI file uses#
from matplotlib.dviread import Dvi
fonts = {}
with Dvi("paper.dvi", None) as dvi:
for page in dvi:
for t in page.text:
fonts[t.font.texname] = t.font
for name, font in sorted(fonts.items()):
print(name.decode(), f"{font.size:.2f} pt")
How to find the font file for a glyph#
Text.font_path resolves the font through the TeX font map (pdftex.map) to a Type 1 or OpenType file:
with Dvi("paper.dvi", 72) as dvi:
page = next(iter(dvi))
t = page.text[0]
print(t.font_path, t.glyph_name_or_index)
find_tex_file looks up any TeX file:
from matplotlib.dviread import find_tex_file
print(find_tex_file("cmr10.tfm")) # e.g. /usr/share/texlive/.../cmr10.tfm
Tips#
- Iterating a page fails with a
FileNotFoundErrorwhen a TFM file isn’t found: install the TeX package that provides the font. - With
dpi=Nonepositions are in DVI units (scaled points for TeX: 65536 per point). python -m matplotlib.dviread paper.dvi 72prints every glyph and box of a file, useful for debugging.