23
23
# - setWeights function needs improvement
24
24
# - 'light' is an invalid weight value, remove it.
25
25
26
+ from enum import IntEnum
26
27
from functools import lru_cache
27
28
import json
28
29
import logging
29
30
from numbers import Number
30
31
import os
31
32
from pathlib import Path
33
+ import re
32
34
import subprocess
33
35
import sys
34
36
try :
35
37
from threading import Timer
36
38
except ImportError :
37
39
from dummy_threading import Timer
38
40
41
+ import numpy as np
42
+
39
43
import matplotlib as mpl
40
44
from matplotlib import afm , cbook , ft2font , rcParams
41
45
from matplotlib .fontconfig_pattern import (
86
90
'extra bold' : 800 ,
87
91
'black' : 900 ,
88
92
}
93
+
94
+
95
+ class _Weight (IntEnum ):
96
+ Thin = 0
97
+ Extralight = Ultralight = 40
98
+ Light = 50
99
+ Demilight = Semilight = 55
100
+ Book = 75
101
+ Regular = Normal = 80
102
+ Medium = 100
103
+ Demibold = Semibold = 180
104
+ Bold = 200
105
+ Extrabold = Ultrabold = 205
106
+ Black = Heavy = 210
107
+ Extrablack = Ultrablack = 215
108
+
109
+ @classmethod
110
+ def from_opentype (cls , ot_weight ):
111
+ fc_weights = [0 , 40 , 50 , 55 , 75 , 80 , 100 , 180 , 200 , 205 , 210 , 215 ]
112
+ ot_weights = [
113
+ 100 , 200 , 300 , 350 , 380 , 400 , 500 , 600 , 700 , 800 , 900 , 1000 ]
114
+ weight = int (np .interp (ot_weight , ot_weights , fc_weights ) + .5 )
115
+ try :
116
+ return _Weight (weight )
117
+ except ValueError :
118
+ return weight
119
+
120
+
121
+ _weight_regexes = [
122
+ ("thin" , _Weight .Thin ),
123
+ ("extralight" , _Weight .Extralight ),
124
+ ("ultralight" , _Weight .Ultralight ),
125
+ ("demilight" , _Weight .Demilight ),
126
+ ("semilight" , _Weight .Semilight ),
127
+ ("light" , _Weight .Light ),
128
+ ("book" , _Weight .Book ),
129
+ ("regular" , _Weight .Regular ),
130
+ ("normal" , _Weight .Normal ),
131
+ ("medium" , _Weight .Medium ),
132
+ ("demibold" , _Weight .Demibold ),
133
+ ("demi" , _Weight .Demibold ),
134
+ ("semibold" , _Weight .Semibold ),
135
+ ("extrabold" , _Weight .Extrabold ),
136
+ ("superbold" , _Weight .Extrabold ),
137
+ ("ultrabold" , _Weight .Ultrabold ),
138
+ ("bold" , _Weight .Bold ),
139
+ ("ultrablack" , _Weight .Ultrablack ),
140
+ ("superblack" , _Weight .Extrablack ),
141
+ ("extrablack" , _Weight .Extrablack ),
142
+ (r"\bultra" , _Weight .Ultrabold ),
143
+ ("black" , _Weight .Black ),
144
+ ("heavy" , _Weight .Heavy ),
145
+ ]
146
+
147
+
89
148
font_family_aliases = {
90
149
'serif' ,
91
150
'sans-serif' ,
95
154
'monospace' ,
96
155
'sans' ,
97
156
}
157
+
158
+
98
159
# OS Font paths
99
160
MSFolders = \
100
161
r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
@@ -356,14 +417,21 @@ def ttfFontProperty(font):
356
417
# Styles are: italic, oblique, and normal (default)
357
418
358
419
sfnt = font .get_sfnt ()
420
+ mac_key = (1 , # platform: macintosh
421
+ 0 , # id: roman
422
+ 0 ) # langid: english
423
+ ms_key = (3 , # platform: microsoft
424
+ 1 , # id: unicode_cs
425
+ 0x0409 ) # langid: english_united_states
426
+
359
427
# These tables are actually mac_roman-encoded, but mac_roman support may be
360
428
# missing in some alternative Python implementations and we are only going
361
429
# to look for ASCII substrings, where any ASCII-compatible encoding works
362
430
# - or big-endian UTF-16, since important Microsoft fonts use that.
363
- sfnt2 = (sfnt .get ((1 , 0 , 0 , 2 ), b'' ).decode ('latin-1' ).lower () or
364
- sfnt .get ((3 , 1 , 0x0409 , 2 ), b'' ).decode ('utf_16_be' ).lower ())
365
- sfnt4 = (sfnt .get ((1 , 0 , 0 , 4 ), b'' ).decode ('latin-1' ).lower () or
366
- sfnt .get ((3 , 1 , 0x0409 , 4 ), b'' ).decode ('utf_16_be' ).lower ())
431
+ sfnt2 = (sfnt .get ((* mac_key , 2 ), b'' ).decode ('latin-1' ).lower () or
432
+ sfnt .get ((* ms_key , 2 ), b'' ).decode ('utf_16_be' ).lower ())
433
+ sfnt4 = (sfnt .get ((* mac_key , 4 ), b'' ).decode ('latin-1' ).lower () or
434
+ sfnt .get ((* ms_key , 4 ), b'' ).decode ('utf_16_be' ).lower ())
367
435
368
436
if sfnt4 .find ('oblique' ) >= 0 :
369
437
style = 'oblique'
@@ -384,10 +452,46 @@ def ttfFontProperty(font):
384
452
else :
385
453
variant = 'normal'
386
454
387
- if font .style_flags & ft2font .BOLD :
388
- weight = 700
389
- else :
390
- weight = next ((w for w in weight_dict if w in sfnt4 ), 400 )
455
+ # The weight-guessing algorithm is directly translated from fontconfig
456
+ # 2.13.1's FcFreeTypeQueryFaceInternal (fcfreetype.c).
457
+ wws_subfamily = 22
458
+ typographic_subfamily = 16
459
+ font_subfamily = 2
460
+ styles = [
461
+ sfnt .get ((* mac_key , wws_subfamily ), b'' ).decode ('latin-1' ),
462
+ sfnt .get ((* mac_key , typographic_subfamily ), b'' ).decode ('latin-1' ),
463
+ sfnt .get ((* mac_key , font_subfamily ), b'' ).decode ('latin-1' ),
464
+ sfnt .get ((* ms_key , wws_subfamily ), b'' ).decode ('utf-16-be' ),
465
+ sfnt .get ((* ms_key , typographic_subfamily ), b'' ).decode ('utf-16-be' ),
466
+ sfnt .get ((* ms_key , font_subfamily ), b'' ).decode ('utf-16-be' ),
467
+ ]
468
+ styles = [* filter (None , styles )] or [font .style_name ]
469
+
470
+ def get_weight ():
471
+ # OS/2 table weight.
472
+ os2 = font .get_sfnt_table ("OS/2" )
473
+ if os2 and os2 ["version" ] != 0xffff :
474
+ return _Weight .from_opentype (os2 ["usWeightClass" ])
475
+ # PostScript font info weight.
476
+ try :
477
+ ps_font_info_weight = (
478
+ font .get_ps_font_info ()["weight" ].replace (" " , "" ) or "" )
479
+ except ValueError :
480
+ pass
481
+ else :
482
+ for regex , weight in _weight_regexes :
483
+ if re .fullmatch (regex , ps_font_info_weight , re .I ):
484
+ return weight
485
+ # Style name weight.
486
+ for style in styles :
487
+ for regex , weight in _weight_regexes :
488
+ if re .search (regex , style .replace (" " , "" ), re .I ):
489
+ return weight
490
+ if font .style_flags & ft2font .BOLD :
491
+ return _Weight .BOLD
492
+ return _Weight .REGULAR
493
+
494
+ weight = int (get_weight ())
391
495
392
496
# Stretch can be absolute and relative
393
497
# Absolute stretches are: ultra-condensed, extra-condensed, condensed,
0 commit comments