1
+ use std:: collections:: HashSet ;
2
+
3
+ use font_types:: NameId ;
1
4
use fontspector_checkapi:: { prelude:: * , skip, testfont, FileTypeConvert } ;
2
5
use skrifa:: MetadataProvider ;
3
6
@@ -8,6 +11,8 @@ const REGULAR_COORDINATE_EXPECTATIONS: [(&str, f32); 4] = [
8
11
( "ital" , 0.0 ) ,
9
12
] ;
10
13
14
+ const REGISTERED_AXIS_TAGS : [ & str ; 5 ] = [ "ital" , "opsz" , "slnt" , "wdth" , "wght" ] ;
15
+
11
16
#[ check(
12
17
id = "opentype/fvar/regular_coords_correct" ,
13
18
title = "Axes and named instances fall within correct ranges?" ,
@@ -137,3 +142,241 @@ fn axis_ranges_correct(t: &Testable, _context: &Context) -> CheckFnResult {
137
142
}
138
143
return_result ( problems)
139
144
}
145
+
146
+ #[ check(
147
+ id = "opentype/varfont/distinct_instance_records" ,
148
+ title = "Validates that all of the instance records in a given font have distinct data" ,
149
+ rationale = "According to the 'fvar' documentation in OpenType spec v1.9
150
+ https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
151
+
152
+ All of the instance records in a font should have distinct coordinates
153
+ and distinct subfamilyNameID and postScriptName ID values. If two or more
154
+ records share the same coordinates, the same nameID values or the same
155
+ postScriptNameID values, then all but the first can be ignored." ,
156
+ proposal = "https://github.com/fonttools/fontbakery/issues/3706"
157
+ ) ]
158
+ fn distinct_instance_records ( t : & Testable , _context : & Context ) -> CheckFnResult {
159
+ let f = testfont ! ( t) ;
160
+ skip ! ( !f. is_variable_font( ) , "not-variable" , "Not a variable font" ) ;
161
+
162
+ let mut problems = vec ! [ ] ;
163
+ let mut unique_records = HashSet :: new ( ) ;
164
+ // We want to get at subfamily and postscript name IDs, so we use the lower-level
165
+ // Skrifa API here.
166
+ for instance in f. font ( ) . named_instances ( ) . iter ( ) {
167
+ let loc = instance. location ( ) ;
168
+ let coords: Vec < _ > = loc. coords ( ) . to_vec ( ) ;
169
+ let subfamily_name_id = instance. subfamily_name_id ( ) ;
170
+ let postscript_name_id = instance. postscript_name_id ( ) ;
171
+ let instance_data = ( coords. clone ( ) , subfamily_name_id, postscript_name_id) ;
172
+ if unique_records. contains ( & instance_data) {
173
+ let subfamily = f
174
+ . get_name_entry_strings ( subfamily_name_id)
175
+ . next ( )
176
+ . unwrap_or_else ( || format ! ( "ID {}" , subfamily_name_id) ) ;
177
+ problems. push ( Status :: warn (
178
+ "duplicate-instance" ,
179
+ & format ! (
180
+ "Instance {} with coordinates {:?} is duplicated" ,
181
+ subfamily, coords
182
+ ) ,
183
+ ) ) ;
184
+ } else {
185
+ unique_records. insert ( instance_data) ;
186
+ }
187
+ }
188
+ return_result ( problems)
189
+ }
190
+
191
+ #[ check(
192
+ id = "opentype/varfont/family_axis_ranges" ,
193
+ title = "Check that family axis ranges are identical" ,
194
+ rationale = "Between members of a family (such as Roman & Italic), the ranges of variable axes must be identical." ,
195
+ proposal = "https://github.com/fonttools/fontbakery/issues/4445" ,
196
+ implementation = "all"
197
+ ) ]
198
+ fn family_axis_ranges ( c : & TestableCollection , context : & Context ) -> CheckFnResult {
199
+ let mut fonts = TTF . from_collection ( c) ;
200
+ fonts. retain ( |f| f. is_variable_font ( ) ) ;
201
+ skip ! (
202
+ fonts. len( ) < 2 ,
203
+ "not-enough-fonts" ,
204
+ "Not enough variable fonts to compare"
205
+ ) ;
206
+ let values: Vec < _ > = fonts
207
+ . iter ( )
208
+ . map ( |f| {
209
+ let label = f
210
+ . filename
211
+ . file_name ( )
212
+ . map ( |x| x. to_string_lossy ( ) )
213
+ . map ( |x| x. to_string ( ) )
214
+ . unwrap_or ( "Unknown file" . to_string ( ) ) ;
215
+ let comparable = f
216
+ . axis_ranges ( )
217
+ . map ( |( ax, min, def, max) | format ! ( "{}={:.2}:{:.2}:{:.2}" , ax, min, def, max) )
218
+ . collect :: < Vec < String > > ( )
219
+ . join ( ", " ) ;
220
+ ( comparable. clone ( ) , comparable, label)
221
+ } )
222
+ . collect ( ) ;
223
+ assert_all_the_same (
224
+ context,
225
+ & values,
226
+ "axis-range-mismatch" ,
227
+ "Variable axis ranges not matching between font files" ,
228
+ )
229
+ }
230
+
231
+ #[ check(
232
+ id = "opentype/varfont/foundry_defined_tag_name" ,
233
+ title = "Validate foundry-defined design-variation axis tag names." ,
234
+ rationale = "According to the OpenType spec's syntactic requirements for
235
+ foundry-defined design-variation axis tags available at
236
+ https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
237
+
238
+ Foundry-defined tags must begin with an uppercase letter
239
+ and must use only uppercase letters or digits." ,
240
+ proposal = "https://github.com/fonttools/fontbakery/issues/4043"
241
+ ) ]
242
+ fn varfont_foundry_defined_tag_name ( t : & Testable , _context : & Context ) -> CheckFnResult {
243
+ let f = testfont ! ( t) ;
244
+ skip ! ( !f. is_variable_font( ) , "not-variable" , "Not a variable font" ) ;
245
+ let mut problems = vec ! [ ] ;
246
+ for axis in f. font ( ) . axes ( ) . iter ( ) {
247
+ let tag = axis. tag ( ) . to_string ( ) ;
248
+ if REGISTERED_AXIS_TAGS . contains ( & tag. as_str ( ) ) {
249
+ continue ;
250
+ }
251
+ if REGISTERED_AXIS_TAGS . contains ( & tag. to_lowercase ( ) . as_str ( ) ) {
252
+ problems. push ( Status :: warn ( "foundry-defined-similar-registered-name" ,
253
+ & format ! ( "Foundry-defined axis tag {} is similar to a registered tag name {}, consider renaming. If this tag was meant to be a registered tag, please use all lowercase letters in the tag name." , tag, tag. to_lowercase( ) )
254
+ ) ) ;
255
+ }
256
+ // Axis tag must be uppercase and contain only uppercase letters or digits
257
+ if !tag
258
+ . chars ( )
259
+ . next ( )
260
+ . map ( |c| c. is_ascii_uppercase ( ) )
261
+ . unwrap_or ( false )
262
+ {
263
+ problems. push ( Status :: fail (
264
+ "invalid-foundry-defined-tag-first-letter" ,
265
+ & format ! (
266
+ "Foundry-defined axis tag {} must begin with an uppercase letter" ,
267
+ tag
268
+ ) ,
269
+ ) )
270
+ } else if !tag
271
+ . chars ( )
272
+ . all ( |c| c. is_ascii_uppercase ( ) || c. is_ascii_digit ( ) )
273
+ {
274
+ problems. push ( Status :: fail ( "invalid-foundry-defined-tag-chars" ,
275
+ & format ! ( "Foundry-defined axis tag {} must begin with an uppercase letter and contain only uppercase letters or digits." , tag)
276
+ ) ) ;
277
+ }
278
+ }
279
+ return_result ( problems)
280
+ }
281
+
282
+ #[ check(
283
+ id = "opentype/varfont/same_size_instance_records" ,
284
+ title = "Validates that all of the instance records in a given font have the same size" ,
285
+ rationale = "According to the 'fvar' documentation in OpenType spec v1.9
286
+ https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
287
+
288
+ All of the instance records in a given font must be the same size, with
289
+ all either including or omitting the postScriptNameID field. [...]
290
+ If the value is 0xFFFF, then the value is ignored, and no PostScript name
291
+ equivalent is provided for the instance." ,
292
+ proposal = "https://github.com/fonttools/fontbakery/issues/3705"
293
+ ) ]
294
+ fn same_size_instance_records ( t : & Testable , _context : & Context ) -> CheckFnResult {
295
+ let f = testfont ! ( t) ;
296
+ skip ! ( !f. is_variable_font( ) , "not-variable" , "Not a variable font" ) ;
297
+ let has_a_postscriptname: HashSet < bool > = f
298
+ . font ( )
299
+ . named_instances ( )
300
+ . iter ( )
301
+ . map ( |ni| ni. postscript_name_id ( ) . is_none ( ) )
302
+ . collect ( ) ;
303
+ Ok ( if has_a_postscriptname. len ( ) > 1 {
304
+ Status :: just_one_fail (
305
+ "different-size-instance-records" ,
306
+ "Instance records don't all have the same size." ,
307
+ )
308
+ } else {
309
+ Status :: just_one_pass ( )
310
+ } )
311
+ }
312
+
313
+ #[ check(
314
+ id = "opentype/varfont/valid_nameids" ,
315
+ title = "Validates that all of the name IDs in an instance record are within the correct range" ,
316
+ rationale = r#"
317
+ According to the 'fvar' documentation in OpenType spec v1.9
318
+ https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
319
+
320
+ The axisNameID field provides a name ID that can be used to obtain strings
321
+ from the 'name' table that can be used to refer to the axis in application
322
+ user interfaces. The name ID must be greater than 255 and less than 32768.
323
+
324
+ The postScriptNameID field provides a name ID that can be used to obtain
325
+ strings from the 'name' table that can be treated as equivalent to name
326
+ ID 6 (PostScript name) strings for the given instance. Values of 6 and
327
+ "undefined" can be used; otherwise, values must be greater than 255 and
328
+ less than 32768.
329
+
330
+ The subfamilyNameID field provides a name ID that can be used to obtain
331
+ strings from the 'name' table that can be treated as equivalent to name
332
+ ID 17 (typographic subfamily) strings for the given instance. Values of
333
+ 2 or 17 can be used; otherwise, values must be greater than 255 and less
334
+ than 32768.
335
+ "# ,
336
+ proposal = "https://github.com/fonttools/fontbakery/issues/3703"
337
+ ) ]
338
+ fn varfont_valid_nameids ( t : & Testable , _context : & Context ) -> CheckFnResult {
339
+ let f = testfont ! ( t) ;
340
+ skip ! ( !f. is_variable_font( ) , "not-variable" , "Not a variable font" ) ;
341
+ let mut problems = vec ! [ ] ;
342
+ let valid_nameid = |n : NameId | ( 255 ..32768 ) . contains ( & n. to_u16 ( ) ) ;
343
+
344
+ // Do the axes first
345
+ for axis in f. font ( ) . axes ( ) . iter ( ) {
346
+ let axis_name_id = axis. name_id ( ) ;
347
+ if !valid_nameid ( axis_name_id) {
348
+ problems. push ( Status :: fail (
349
+ "invalid-axis-name-id" ,
350
+ & format ! (
351
+ "Axis name ID {} ({}) is out of range. It must be greater than 255 and less than 32768." ,
352
+ axis_name_id, f. get_name_entry_strings( axis_name_id) . next( ) . unwrap_or_default( )
353
+ ) ,
354
+ ) ) ;
355
+ }
356
+ }
357
+
358
+ for instance in f. font ( ) . named_instances ( ) . iter ( ) {
359
+ let subfamily_name_id = instance. subfamily_name_id ( ) ;
360
+ if let Some ( n) = instance. postscript_name_id ( ) {
361
+ if n != NameId :: new ( 6 ) && !valid_nameid ( n) {
362
+ problems. push ( Status :: fail (
363
+ "invalid-postscript-name-id" ,
364
+ & format ! (
365
+ "PostScript name ID {} ({}) is out of range. It must be greater than 255 and less than 32768, or 6 or 0xFFFF." ,
366
+ n, f. get_name_entry_strings( n) . next( ) . unwrap_or_default( )
367
+ ) ,
368
+ ) ) ;
369
+ }
370
+ }
371
+ if !valid_nameid ( subfamily_name_id) {
372
+ problems. push ( Status :: fail (
373
+ "invalid-subfamily-name-id" ,
374
+ & format ! (
375
+ "Instance subfamily name ID {} ({}) is out of range. It must be greater than 255 and less than 32768." ,
376
+ subfamily_name_id, f. get_name_entry_strings( subfamily_name_id) . next( ) . unwrap_or_default( )
377
+ ) ,
378
+ ) ) ;
379
+ }
380
+ }
381
+ return_result ( problems)
382
+ }
0 commit comments