@@ -3,6 +3,7 @@ use read_fonts::{
3
3
tables:: { head:: MacStyle , os2:: SelectionFlags } ,
4
4
TableProvider ,
5
5
} ;
6
+ use skrifa:: { GlyphId , MetadataProvider } ;
6
7
7
8
#[ check(
8
9
id = "opentype/fsselection" ,
@@ -160,3 +161,102 @@ fn check_vendor_id(f: &Testable, context: &Context) -> CheckFnResult {
160
161
) )
161
162
}
162
163
}
164
+
165
+ const AVG_CHAR_WEIGHTS : [ ( char , u32 ) ; 27 ] = [
166
+ ( 'a' , 64 ) ,
167
+ ( 'b' , 14 ) ,
168
+ ( 'c' , 27 ) ,
169
+ ( 'd' , 35 ) ,
170
+ ( 'e' , 100 ) ,
171
+ ( 'f' , 20 ) ,
172
+ ( 'g' , 14 ) ,
173
+ ( 'h' , 42 ) ,
174
+ ( 'i' , 63 ) ,
175
+ ( 'j' , 3 ) ,
176
+ ( 'k' , 6 ) ,
177
+ ( 'l' , 35 ) ,
178
+ ( 'm' , 20 ) ,
179
+ ( 'n' , 56 ) ,
180
+ ( 'o' , 56 ) ,
181
+ ( 'p' , 17 ) ,
182
+ ( 'q' , 4 ) ,
183
+ ( 'r' , 49 ) ,
184
+ ( 's' , 56 ) ,
185
+ ( 't' , 71 ) ,
186
+ ( 'u' , 31 ) ,
187
+ ( 'v' , 10 ) ,
188
+ ( 'w' , 18 ) ,
189
+ ( 'x' , 3 ) ,
190
+ ( 'y' , 18 ) ,
191
+ ( 'z' , 2 ) ,
192
+ ( ' ' , 166 ) ,
193
+ ] ;
194
+
195
+ #[ check(
196
+ id = "opentype/xavgcharwidth" ,
197
+ proposal = "https://github.com/fonttools/fontbakery/issues/4829" ,
198
+ title = "Checking OS/2 fsSelection value." ,
199
+ rationale = "
200
+ The OS/2.xAvgCharWidth field is used to calculate the width of a string of
201
+ characters. It is the average width of all non-zero width glyphs in the font.
202
+
203
+ This check ensures that the value is correct. A failure here may indicate
204
+ a bug in the font compiler, rather than something that the designer can
205
+ do anything about.
206
+ "
207
+ ) ]
208
+ fn xavgcharwidth ( f : & Testable , _context : & Context ) -> CheckFnResult {
209
+ let font = testfont ! ( f) ;
210
+ let os2 = font. font ( ) . os2 ( ) ?;
211
+ let hmtx = font. font ( ) . hmtx ( ) ?;
212
+ let charmap = font. font ( ) . charmap ( ) ;
213
+ let ( rule, expected) = if os2. version ( ) >= 3 {
214
+ let advances = hmtx
215
+ . h_metrics ( )
216
+ . iter ( )
217
+ . map ( |metric| metric. advance . get ( ) as u32 )
218
+ . filter ( |& w| w > 0 )
219
+ . collect :: < Vec < _ > > ( ) ;
220
+ (
221
+ "the average of the widths of all glyphs in the font" ,
222
+ advances. iter ( ) . sum :: < u32 > ( ) / advances. len ( ) as u32 ,
223
+ )
224
+ } else {
225
+ let ids: Vec < Option < GlyphId > > = AVG_CHAR_WEIGHTS
226
+ . iter ( )
227
+ . map ( |( c, _) | charmap. map ( * c) )
228
+ . collect ( ) ;
229
+ if ids. iter ( ) . any ( |id| id. is_none ( ) ) {
230
+ return Err ( CheckError :: Error (
231
+ "Missing glyph in font for average character width calculation" . to_string ( ) ,
232
+ ) ) ;
233
+ }
234
+ #[ allow( clippy:: unwrap_used) ] // We know all the characters are in the font
235
+ let advances = ids
236
+ . iter ( )
237
+ . zip ( AVG_CHAR_WEIGHTS . iter ( ) )
238
+ . map ( |( id, ( _, w) ) | hmtx. advance ( id. unwrap ( ) ) . unwrap_or ( 0 ) as u32 * w)
239
+ . collect :: < Vec < _ > > ( ) ;
240
+ (
241
+ "the weighted average of the widths of the latin lowercase glyphs in the font" ,
242
+ advances. iter ( ) . sum :: < u32 > ( ) / advances. len ( ) as u32 ,
243
+ )
244
+ } ;
245
+ let actual = os2. x_avg_char_width ( ) ;
246
+ let difference = ( expected as i16 - actual) . abs ( ) ;
247
+ Ok ( match difference {
248
+ 0 |1 => Status :: just_one_pass ( ) ,
249
+ 2 |3 |4 |5 |6 |7 |8 |9 |20 => Status :: just_one_info (
250
+ "xAvgCharWidth-close" ,
251
+ & format ! ( "OS/2 xAvgCharWidth is {} but it should be {} which corresponds to {}. These are similar values, which may be a symptom of the slightly different calculation of the xAvgCharWidth value in font editors. There's further discussion on this at https://github.com/fonttools/fontbakery/issues/1622" ,
252
+ actual, expected, rule
253
+ )
254
+ ) ,
255
+ _ => Status :: just_one_warn (
256
+ "xAvgCharWidth-wrong" ,
257
+ & format ! ( "OS/2 xAvgCharWidth is {} but it should be {} which corresponds to {}. This may indicate a problem with the font editor or the font compiler." ,
258
+ actual, expected, rule
259
+ )
260
+ )
261
+ } )
262
+ }
0 commit comments