3030
3131import numpy as np
3232
33- from matplotlib import _api ,cbook
33+ from matplotlib import _api ,cbook ,textpath
34+ from matplotlib .ft2font import FT2Font ,LoadFlags
3435
3536_log = logging .getLogger (__name__ )
3637
@@ -106,18 +107,29 @@ def font_effects(self):
106107@property
107108def glyph_name_or_index (self ):
108109"""
109- Either the glyph name or the native charmap glyph index.
110-
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.
116-
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.
110+ The glyph name, the native charmap glyph index, or the raw glyph index.
111+
112+ If the font is a TrueType file (which can currently only happen for
113+ DVI files generated by luatex), then this number is the raw index of
114+ the glyph, which can be passed to FT_Load_Glyph/load_glyph. Note that
115+ xetex is currently unsupported and the behavior on xdv files (xetex's
116+ version of dvi) is undefined.
117+
118+ Otherwise, the font is a PostScript font. For such fonts, if
119+ :file:`pdftex.map` specifies an encoding for this glyph's font,
120+ that is a mapping of glyph indices to Adobe glyph names; which
121+ is used by this property to convert dvi numbers to glyph names.
122+ Callers can then convert glyph names to glyph indices (with
123+ FT_Get_Name_Index/get_name_index), and load the glyph using
124+ FT_Load_Glyph/load_glyph.
125+
126+ If :file:`pdftex.map` specifies no encoding for a PostScript font,
127+ this number is an index to the font's "native" charmap; glyphs should
128+ directly load using FT_Load_Char/load_char after selecting the native
129+ charmap.
120130 """
131+ # TODO: The last section is only true since luaotfload 3.15; add a
132+ # version check in the tex file generated by texmanager.
121133entry = self ._get_pdftexmap_entry ()
122134return (_parse_enc (entry .encoding )[self .glyph ]
123135if entry .encoding is not None else self .glyph )
@@ -399,7 +411,7 @@ def _put_char_real(self, char):
399411scale = font ._scale
400412for x ,y ,f ,g ,w in font ._vf [char ].text :
401413newf = DviFont (scale = _mul2012 (scale ,f ._scale ),
402- tfm = f ._tfm ,texname = f .texname ,vf = f ._vf )
414+ metrics = f ._metrics ,texname = f .texname ,vf = f ._vf )
403415self .text .append (Text (self .h + _mul2012 (x ,scale ),
404416self .v + _mul2012 (y ,scale ),
405417newf ,g ,newf ._width_of (g )))
@@ -496,6 +508,12 @@ def _fnt_def(self, k, c, s, d, a, l):
496508def _fnt_def_real (self ,k ,c ,s ,d ,a ,l ):
497509n = self .file .read (a + l )
498510fontname = n [- l :].decode ('ascii' )
511+ # TODO: Implement full spec, https://tug.org/pipermail/dvipdfmx/2021-January/000168.html
512+ # Note that checksum seems wrong?
513+ if fontname .startswith ('[' )and fontname .endswith (']' ):
514+ metrics = TtfMetrics (fontname [1 :- 1 ])
515+ self .fonts [k ]= DviFont (scale = s ,metrics = metrics ,texname = n ,vf = None )
516+ return
499517try :
500518tfm = _tfmfile (fontname )
501519except FileNotFoundError as exc :
@@ -512,7 +530,7 @@ def _fnt_def_real(self, k, c, s, d, a, l):
512530vf = _vffile (fontname )
513531except FileNotFoundError :
514532vf = None
515- self .fonts [k ]= DviFont (scale = s ,tfm = tfm ,texname = n ,vf = vf )
533+ self .fonts [k ]= DviFont (scale = s ,metrics = tfm ,texname = n ,vf = vf )
516534
517535@_dispatch (247 ,state = _dvistate .pre ,args = ('u1' ,'u4' ,'u4' ,'u4' ,'u1' ))
518536def _pre (self ,i ,num ,den ,mag ,k ):
@@ -562,7 +580,7 @@ class DviFont:
562580 ----------
563581 scale : float
564582 Factor by which the font is scaled from its natural size.
565- tfm : Tfm
583+ tfm : Tfm | TtfMetrics
566584 TeX font metrics for this font
567585 texname : bytes
568586 Name of the font as used internally by TeX and friends, as an ASCII
@@ -582,21 +600,17 @@ class DviFont:
582600 the point size.
583601
584602 """
585- __slots__ = ('texname' ,'size' ,'widths' , ' _scale' ,'_vf' ,'_tfm ' )
603+ __slots__ = ('texname' ,'size' ,'_scale' ,'_vf' ,'_metrics ' )
586604
587- def __init__ (self ,scale ,tfm ,texname ,vf ):
605+ def __init__ (self ,scale ,metrics ,texname ,vf ):
588606_api .check_isinstance (bytes ,texname = texname )
589607self ._scale = scale
590- self ._tfm = tfm
608+ self ._metrics = metrics
591609self .texname = texname
592610self ._vf = vf
593611self .size = scale * (72.0 / (72.27 * 2 ** 16 ))
594- try :
595- nchars = max (tfm .width )+ 1
596- except ValueError :
597- nchars = 0
598- self .widths = [(1000 * tfm .width .get (char ,0 ))>> 20
599- for char in range (nchars )]
612+
613+ widths = _api .deprecated ("3.11" )(property (lambda self : ...))
600614
601615def __eq__ (self ,other ):
602616return (type (self )is type (other )
@@ -610,32 +624,30 @@ def __repr__(self):
610624
611625def _width_of (self ,char ):
612626"""Width of char in dvi units."""
613- width = self ._tfm . width . get (char , None )
614- if width is not None :
615- return _mul2012 ( width , self ._scale )
616- _log . debug ( 'No width for char %d in font %s.' , char , self . texname )
617- return 0
627+ metrics = self ._metrics . get_metrics (char )
628+ if metrics is None :
629+ _log . debug ( 'No width for char %d in font %s.' , char , self .texname )
630+ return 0
631+ return _mul2012 ( metrics . width , self . _scale )
618632
619633def _height_depth_of (self ,char ):
620634"""Height and depth of char in dvi units."""
621- result = []
622- for metric ,name in ((self ._tfm .height ,"height" ),
623- (self ._tfm .depth ,"depth" )):
624- value = metric .get (char ,None )
625- if value is None :
626- _log .debug ('No %s for char %d in font %s' ,
627- name ,char ,self .texname )
628- result .append (0 )
629- else :
630- result .append (_mul2012 (value ,self ._scale ))
635+ metrics = self ._metrics .get_metrics (char )
636+ if metrics is None :
637+ _log .debug ('No metrics for char %d in font %s' ,char ,self .texname )
638+ return [0 ,0 ]
639+ metrics = [
640+ _mul2012 (metrics .height ,self ._scale ),
641+ _mul2012 (metrics .depth ,self ._scale ),
642+ ]
631643# cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent
632644# so that TeX aligns equations properly
633645# (https://tex.stackexchange.com/q/526103/)
634646# but we actually care about the rasterization depth to align
635647# the dvipng-generated images.
636648if re .match (br'^cmsy\d+$' ,self .texname )and char == 0 :
637- result [- 1 ]= 0
638- return result
649+ metrics [- 1 ]= 0
650+ return metrics
639651
640652
641653class Vf (Dvi ):
@@ -767,6 +779,9 @@ def _mul2012(num1, num2):
767779return (num1 * num2 )>> 20
768780
769781
782+ WHD = namedtuple ('WHD' ,'width height depth' )
783+
784+
770785class Tfm :
771786"""
772787 A TeX Font Metric file.
@@ -782,13 +797,13 @@ class Tfm:
782797 checksum : int
783798 Used for verifying against the dvi file.
784799 design_size : int
785- Design size of the font (unknown units)
800+ Design size of the font (unknown units).
786801 width, height, depth : dict
787802 Dimensions of each character, need to be scaled by the factor
788803 specified in the dvi file. These are dicts because indexing may
789804 not start from 0.
790805 """
791- __slots__ = ('checksum' ,'design_size' ,'width ' ,'height' , 'depth ' )
806+ __slots__ = ('checksum' ,'design_size' ,'_whds ' ,'widths ' )
792807
793808def __init__ (self ,filename ):
794809_log .debug ('opening tfm file %s' ,filename )
@@ -804,15 +819,36 @@ def __init__(self, filename):
804819widths = struct .unpack (f'!{ nw } i' ,file .read (4 * nw ))
805820heights = struct .unpack (f'!{ nh } i' ,file .read (4 * nh ))
806821depths = struct .unpack (f'!{ nd } i' ,file .read (4 * nd ))
807- self .width = {}
808- self .height = {}
809- self .depth = {}
822+ self ._whds = {}
810823for idx ,char in enumerate (range (bc ,ec + 1 )):
811824byte0 = char_info [4 * idx ]
812825byte1 = char_info [4 * idx + 1 ]
813- self .width [char ]= widths [byte0 ]
814- self .height [char ]= heights [byte1 >> 4 ]
815- self .depth [char ]= depths [byte1 & 0xf ]
826+ self ._whds [char ]= WHD (
827+ widths [byte0 ],heights [byte1 >> 4 ],depths [byte1 & 0xf ])
828+ self .widths = [(1000 * self ._whds [c ].width if c in self ._whds else 0 )>> 20
829+ for c in range (max (self ._whds ))]if self ._whds else []
830+
831+ def get_metrics (self ,char ):
832+ return self ._whds [char ]
833+
834+ width = _api .deprecated ("3.11" )(
835+ property (lambda self : {c :m .width for c ,m in self ._whds }))
836+ height = _api .deprecated ("3.11" )(
837+ property (lambda self : {c :m .height for c ,m in self ._whds }))
838+ depth = _api .deprecated ("3.11" )(
839+ property (lambda self : {c :m .depth for c ,m in self ._whds }))
840+
841+
842+ class TtfMetrics :
843+ def __init__ (self ,filename ):
844+ self ._face = FT2Font (filename ,hinting_factor = 1 )# Manage closing?
845+
846+ def get_metrics (self ,char ):
847+ mul = self ._face .units_per_EM
848+ g = self ._face .load_glyph (char ,LoadFlags .NO_SCALE )
849+ return WHD (g .horiAdvance * mul ,
850+ g .height * mul ,
851+ (g .height - g .horiBearingY )* mul )
816852
817853
818854PsFont = namedtuple ('PsFont' ,'texname psname effects encoding filename' )
@@ -1007,8 +1043,7 @@ def _parse_enc(path):
10071043 Returns
10081044 -------
10091045 list
1010- The nth entry of the list is the PostScript glyph name of the nth
1011- glyph.
1046+ The nth list item is the PostScript glyph name of the nth glyph.
10121047 """
10131048no_comments = re .sub ("%.*" ,"" ,Path (path ).read_text (encoding = "ascii" ))
10141049array = re .search (r"(?s)\[(.*)\]" ,no_comments ).group (1 )
@@ -1113,26 +1148,45 @@ def _fontfile(cls, suffix, texname):
11131148from argparse import ArgumentParser
11141149import itertools
11151150
1151+ import fontTools .agl
1152+
11161153parser = ArgumentParser ()
11171154parser .add_argument ("filename" )
11181155parser .add_argument ("dpi" ,nargs = "?" ,type = float ,default = None )
11191156args = parser .parse_args ()
11201157with Dvi (args .filename ,args .dpi )as dvi :
11211158fontmap = PsfontsMap (find_tex_file ('pdftex.map' ))
11221159for page in dvi :
1123- print (f"===new page === "
1160+ print (f"===NEW PAGE === "
11241161f"(w:{ page .width } , h:{ page .height } , d:{ page .descent } )" )
1125- for font ,group in itertools .groupby (
1126- page .text ,lambda text :text .font ):
1127- print (f"font:{ font .texname .decode ('latin-1' )!r} \t "
1128- f"scale:{ font ._scale / 2 ** 20 } " )
1129- print ("x" ,"y" ,"glyph" ,"chr" ,"w" ,"(glyphs)" ,sep = "\t " )
1162+ print ("--- GLYPHS ---" )
1163+ for font ,group in itertools .groupby (page .text ,lambda text :text .font ):
1164+ font_name = font .texname .decode ("latin-1" )
1165+ filename = (font_name [1 :- 1 ]if font_name .startswith ("[" )
1166+ else fontmap [font .texname ].filename )
1167+ if font_name .startswith ("[" ):
1168+ print (f"font:{ font_name } " )
1169+ else :
1170+ print (f"font:{ font_name } at{ filename } " )
1171+ print (f"scale:{ font ._scale / 2 ** 20 } " )
1172+ print (" " .join (map ("{:>11}" .format , ["x" ,"y" ,"glyph" ,"chr" ,"w" ])))
1173+ face = FT2Font (filename )
11301174for text in group :
1131- print (text .x ,text .y ,text .glyph ,
1132- chr (text .glyph )if chr (text .glyph ).isprintable ()
1133- else "." ,
1134- text .width ,sep = "\t " )
1175+ if font_name .startswith ("[" ):
1176+ glyph_name = face .get_glyph_name (text .glyph )
1177+ else :
1178+ if isinstance (text .glyph_name_or_index ,str ):
1179+ glyph_name = text .glyph_name_or_index
1180+ else :
1181+ textpath .TextToPath ._select_native_charmap (face )
1182+ glyph_name = face .get_glyph_name (
1183+ face .get_char_index (text .glyph ))
1184+ glyph_str = fontTools .agl .toUnicode (glyph_name )
1185+ print (" " .join (map ("{:>11}" .format , [
1186+ text .x ,text .y ,text .glyph ,glyph_str ,text .width ])))
11351187if page .boxes :
1136- print ("x" ,"y" ,"h" ,"w" ,"" ,"(boxes)" ,sep = "\t " )
1188+ print ("--- BOXES ---" )
1189+ print (" " .join (map ("{:>11}" .format , ["x" ,"y" ,"h" ,"w" ])))
11371190for box in page .boxes :
1138- print (box .x ,box .y ,box .height ,box .width ,sep = "\t " )
1191+ print (" " .join (map ("{:>11}" .format , [
1192+ box .x ,box .y ,box .height ,box .width ])))