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
107108 def 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.
121133 entry = self ._get_pdftexmap_entry ()
122134 return (_parse_enc (entry .encoding )[self .glyph ]
123135 if entry .encoding is not None else self .glyph )
@@ -399,7 +411,7 @@ def _put_char_real(self, char):
399411 scale = font ._scale
400412 for x , y , f , g , w in font ._vf [char ].text :
401413 newf = 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 )
403415 self .text .append (Text (self .h + _mul2012 (x , scale ),
404416 self .v + _mul2012 (y , scale ),
405417 newf , g , newf ._width_of (g )))
@@ -496,6 +508,12 @@ def _fnt_def(self, k, c, s, d, a, l):
496508 def _fnt_def_real (self , k , c , s , d , a , l ):
497509 n = self .file .read (a + l )
498510 fontname = 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
499517 try :
500518 tfm = _tfmfile (fontname )
501519 except FileNotFoundError as exc :
@@ -512,7 +530,7 @@ def _fnt_def_real(self, k, c, s, d, a, l):
512530 vf = _vffile (fontname )
513531 except FileNotFoundError :
514532 vf = 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' ))
518536 def _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 )
589607 self ._scale = scale
590- self ._tfm = tfm
608+ self ._metrics = metrics
591609 self .texname = texname
592610 self ._vf = vf
593611 self .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
601615 def __eq__ (self , other ):
602616 return (type (self ) is type (other )
@@ -610,32 +624,30 @@ def __repr__(self):
610624
611625 def _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
619633 def _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.
636648 if 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):
767779 return (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
793808 def __init__ (self , filename ):
794809 _log .debug ('opening tfm file %s' , filename )
@@ -804,15 +819,37 @@ def __init__(self, filename):
804819 widths = struct .unpack (f'!{ nw } i' , file .read (4 * nw ))
805820 heights = struct .unpack (f'!{ nh } i' , file .read (4 * nh ))
806821 depths = struct .unpack (f'!{ nd } i' , file .read (4 * nd ))
807- self .width = {}
808- self .height = {}
809- self .depth = {}
822+ self ._whds = {}
810823 for idx , char in enumerate (range (bc , ec + 1 )):
811824 byte0 = char_info [4 * idx ]
812825 byte1 = 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+ textpath .TextToPath ._select_native_charmap (self ._face )
846+
847+ def get_metrics (self , char ):
848+ mul = self ._face .units_per_EM
849+ g = self ._face .load_glyph (char , LoadFlags .NO_SCALE )
850+ return WHD (g .horiAdvance * mul ,
851+ g .height * mul ,
852+ (g .height - g .horiBearingY ) * mul )
816853
817854
818855PsFont = namedtuple ('PsFont' , 'texname psname effects encoding filename' )
@@ -1007,8 +1044,7 @@ def _parse_enc(path):
10071044 Returns
10081045 -------
10091046 list
1010- The nth entry of the list is the PostScript glyph name of the nth
1011- glyph.
1047+ The nth list item is the PostScript glyph name of the nth glyph.
10121048 """
10131049 no_comments = re .sub ("%.*" , "" , Path (path ).read_text (encoding = "ascii" ))
10141050 array = re .search (r"(?s)\[(.*)\]" , no_comments ).group (1 )
@@ -1113,26 +1149,45 @@ def _fontfile(cls, suffix, texname):
11131149 from argparse import ArgumentParser
11141150 import itertools
11151151
1152+ import fontTools .agl
1153+
11161154 parser = ArgumentParser ()
11171155 parser .add_argument ("filename" )
11181156 parser .add_argument ("dpi" , nargs = "?" , type = float , default = None )
11191157 args = parser .parse_args ()
11201158 with Dvi (args .filename , args .dpi ) as dvi :
11211159 fontmap = PsfontsMap (find_tex_file ('pdftex.map' ))
11221160 for page in dvi :
1123- print (f"=== new page === "
1161+ print (f"=== NEW PAGE === "
11241162 f"(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 " )
1163+ print ("--- GLYPHS ---" )
1164+ for font , group in itertools .groupby (page .text , lambda text : text .font ):
1165+ font_name = font .texname .decode ("latin-1" )
1166+ filename = (font_name [1 :- 1 ] if font_name .startswith ("[" )
1167+ else fontmap [font .texname ].filename )
1168+ if font_name .startswith ("[" ):
1169+ print (f"font: { font_name } " )
1170+ else :
1171+ print (f"font: { font_name } at { filename } " )
1172+ print (f"scale: { font ._scale / 2 ** 20 } " )
1173+ print (" " .join (map ("{:>11}" .format , ["x" , "y" , "glyph" , "chr" , "w" ])))
1174+ face = FT2Font (filename )
1175+ textpath .TextToPath ._select_native_charmap (face )
11301176 for 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 " )
1177+ if font_name .startswith ("[" ):
1178+ glyph_name = face .get_glyph_name (text .glyph )
1179+ else :
1180+ if isinstance (text .glyph_name_or_index , str ):
1181+ glyph_name = text .glyph_name_or_index
1182+ else :
1183+ glyph_name = face .get_glyph_name (
1184+ face .get_char_index (text .glyph ))
1185+ glyph_str = fontTools .agl .toUnicode (glyph_name )
1186+ print (" " .join (map ("{:>11}" .format , [
1187+ text .x , text .y , text .glyph , glyph_str , text .width ])))
11351188 if page .boxes :
1136- print ("x" , "y" , "h" , "w" , "" , "(boxes)" , sep = "\t " )
1189+ print ("--- BOXES ---" )
1190+ print (" " .join (map ("{:>11}" .format , ["x" , "y" , "h" , "w" ])))
11371191 for box in page .boxes :
1138- print (box .x , box .y , box .height , box .width , sep = "\t " )
1192+ print (" " .join (map ("{:>11}" .format , [
1193+ box .x , box .y , box .height , box .width ])))
0 commit comments