2323# - setWeights function needs improvement
2424# - 'light' is an invalid weight value, remove it.
2525
26+ from enum import IntEnum
2627from functools import lru_cache
2728import json
2829import logging
2930from numbers import Number
3031import os
3132from pathlib import Path
33+ import re
3234import subprocess
3335import sys
3436try :
3537 from threading import Timer
3638except ImportError :
3739 from dummy_threading import Timer
3840
41+ import numpy as np
42+
3943import matplotlib as mpl
4044from matplotlib import afm , cbook , ft2font , rcParams
4145from matplotlib .fontconfig_pattern import (
8690 'extra bold' : 800 ,
8791 'black' : 900 ,
8892}
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+
89148font_family_aliases = {
90149 'serif' ,
91150 'sans-serif' ,
95154 'monospace' ,
96155 'sans' ,
97156}
157+
158+
98159# OS Font paths
99160MSFolders = \
100161 r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
@@ -356,14 +417,21 @@ def ttfFontProperty(font):
356417 # Styles are: italic, oblique, and normal (default)
357418
358419 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+
359427 # These tables are actually mac_roman-encoded, but mac_roman support may be
360428 # missing in some alternative Python implementations and we are only going
361429 # to look for ASCII substrings, where any ASCII-compatible encoding works
362430 # - 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 ())
367435
368436 if sfnt4 .find ('oblique' ) >= 0 :
369437 style = 'oblique'
@@ -384,10 +452,46 @@ def ttfFontProperty(font):
384452 else :
385453 variant = 'normal'
386454
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 ())
391495
392496 # Stretch can be absolute and relative
393497 # Absolute stretches are: ultra-condensed, extra-condensed, condensed,
0 commit comments