30
30
31
31
import numpy as np
32
32
33
- from matplotlib import _api , cbook
33
+ from matplotlib import _api , cbook , textpath
34
+ from matplotlib .ft2font import FT2Font , LoadFlags
34
35
35
36
_log = logging .getLogger (__name__ )
36
37
@@ -106,18 +107,29 @@ def font_effects(self):
106
107
@property
107
108
def glyph_name_or_index (self ):
108
109
"""
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.
120
130
"""
131
+ # TODO: The last section is only true since luaotfload 3.15; add a
132
+ # version check in the tex file generated by texmanager.
121
133
entry = self ._get_pdftexmap_entry ()
122
134
return (_parse_enc (entry .encoding )[self .glyph ]
123
135
if entry .encoding is not None else self .glyph )
@@ -399,7 +411,7 @@ def _put_char_real(self, char):
399
411
scale = font ._scale
400
412
for x , y , f , g , w in font ._vf [char ].text :
401
413
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 )
403
415
self .text .append (Text (self .h + _mul2012 (x , scale ),
404
416
self .v + _mul2012 (y , scale ),
405
417
newf , g , newf ._width_of (g )))
@@ -496,6 +508,12 @@ def _fnt_def(self, k, c, s, d, a, l):
496
508
def _fnt_def_real (self , k , c , s , d , a , l ):
497
509
n = self .file .read (a + l )
498
510
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
499
517
try :
500
518
tfm = _tfmfile (fontname )
501
519
except FileNotFoundError as exc :
@@ -512,7 +530,7 @@ def _fnt_def_real(self, k, c, s, d, a, l):
512
530
vf = _vffile (fontname )
513
531
except FileNotFoundError :
514
532
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 )
516
534
517
535
@_dispatch (247 , state = _dvistate .pre , args = ('u1' , 'u4' , 'u4' , 'u4' , 'u1' ))
518
536
def _pre (self , i , num , den , mag , k ):
@@ -562,7 +580,7 @@ class DviFont:
562
580
----------
563
581
scale : float
564
582
Factor by which the font is scaled from its natural size.
565
- tfm : Tfm
583
+ tfm : Tfm | TtfMetrics
566
584
TeX font metrics for this font
567
585
texname : bytes
568
586
Name of the font as used internally by TeX and friends, as an ASCII
@@ -582,21 +600,17 @@ class DviFont:
582
600
the point size.
583
601
584
602
"""
585
- __slots__ = ('texname' , 'size' , 'widths' , ' _scale' , '_vf' , '_tfm ' )
603
+ __slots__ = ('texname' , 'size' , '_scale' , '_vf' , '_metrics ' )
586
604
587
- def __init__ (self , scale , tfm , texname , vf ):
605
+ def __init__ (self , scale , metrics , texname , vf ):
588
606
_api .check_isinstance (bytes , texname = texname )
589
607
self ._scale = scale
590
- self ._tfm = tfm
608
+ self ._metrics = metrics
591
609
self .texname = texname
592
610
self ._vf = vf
593
611
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 : ...))
600
614
601
615
def __eq__ (self , other ):
602
616
return (type (self ) is type (other )
@@ -610,32 +624,30 @@ def __repr__(self):
610
624
611
625
def _width_of (self , char ):
612
626
"""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 )
618
632
619
633
def _height_depth_of (self , char ):
620
634
"""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
+ ]
631
643
# cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent
632
644
# so that TeX aligns equations properly
633
645
# (https://tex.stackexchange.com/q/526103/)
634
646
# but we actually care about the rasterization depth to align
635
647
# the dvipng-generated images.
636
648
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
639
651
640
652
641
653
class Vf (Dvi ):
@@ -767,6 +779,9 @@ def _mul2012(num1, num2):
767
779
return (num1 * num2 ) >> 20
768
780
769
781
782
+ WHD = namedtuple ('WHD' , 'width height depth' )
783
+
784
+
770
785
class Tfm :
771
786
"""
772
787
A TeX Font Metric file.
@@ -782,13 +797,13 @@ class Tfm:
782
797
checksum : int
783
798
Used for verifying against the dvi file.
784
799
design_size : int
785
- Design size of the font (unknown units)
800
+ Design size of the font (unknown units).
786
801
width, height, depth : dict
787
802
Dimensions of each character, need to be scaled by the factor
788
803
specified in the dvi file. These are dicts because indexing may
789
804
not start from 0.
790
805
"""
791
- __slots__ = ('checksum' , 'design_size' , 'width ' , 'height' , 'depth ' )
806
+ __slots__ = ('checksum' , 'design_size' , '_whds ' , 'widths ' )
792
807
793
808
def __init__ (self , filename ):
794
809
_log .debug ('opening tfm file %s' , filename )
@@ -804,15 +819,36 @@ def __init__(self, filename):
804
819
widths = struct .unpack (f'!{ nw } i' , file .read (4 * nw ))
805
820
heights = struct .unpack (f'!{ nh } i' , file .read (4 * nh ))
806
821
depths = struct .unpack (f'!{ nd } i' , file .read (4 * nd ))
807
- self .width = {}
808
- self .height = {}
809
- self .depth = {}
822
+ self ._whds = {}
810
823
for idx , char in enumerate (range (bc , ec + 1 )):
811
824
byte0 = char_info [4 * idx ]
812
825
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
+
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 )
816
852
817
853
818
854
PsFont = namedtuple ('PsFont' , 'texname psname effects encoding filename' )
@@ -1007,8 +1043,7 @@ def _parse_enc(path):
1007
1043
Returns
1008
1044
-------
1009
1045
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.
1012
1047
"""
1013
1048
no_comments = re .sub ("%.*" , "" , Path (path ).read_text (encoding = "ascii" ))
1014
1049
array = re .search (r"(?s)\[(.*)\]" , no_comments ).group (1 )
@@ -1113,26 +1148,45 @@ def _fontfile(cls, suffix, texname):
1113
1148
from argparse import ArgumentParser
1114
1149
import itertools
1115
1150
1151
+ import fontTools .agl
1152
+
1116
1153
parser = ArgumentParser ()
1117
1154
parser .add_argument ("filename" )
1118
1155
parser .add_argument ("dpi" , nargs = "?" , type = float , default = None )
1119
1156
args = parser .parse_args ()
1120
1157
with Dvi (args .filename , args .dpi ) as dvi :
1121
1158
fontmap = PsfontsMap (find_tex_file ('pdftex.map' ))
1122
1159
for page in dvi :
1123
- print (f"=== new page === "
1160
+ print (f"=== NEW PAGE === "
1124
1161
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 " )
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 )
1130
1174
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 " )
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 ])))
1135
1187
if page .boxes :
1136
- print ("x" , "y" , "h" , "w" , "" , "(boxes)" , sep = "\t " )
1188
+ print ("--- BOXES ---" )
1189
+ print (" " .join (map ("{:>11}" .format , ["x" , "y" , "h" , "w" ])))
1137
1190
for 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 ])))
0 commit comments