fonttools 4.58.3__cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fonttools might be problematic. Click here for more details.

Files changed (334) hide show
  1. fontTools/__init__.py +8 -0
  2. fontTools/__main__.py +35 -0
  3. fontTools/afmLib.py +439 -0
  4. fontTools/agl.py +5233 -0
  5. fontTools/cffLib/CFF2ToCFF.py +203 -0
  6. fontTools/cffLib/CFFToCFF2.py +305 -0
  7. fontTools/cffLib/__init__.py +3694 -0
  8. fontTools/cffLib/specializer.py +927 -0
  9. fontTools/cffLib/transforms.py +490 -0
  10. fontTools/cffLib/width.py +210 -0
  11. fontTools/colorLib/__init__.py +0 -0
  12. fontTools/colorLib/builder.py +664 -0
  13. fontTools/colorLib/errors.py +2 -0
  14. fontTools/colorLib/geometry.py +143 -0
  15. fontTools/colorLib/table_builder.py +223 -0
  16. fontTools/colorLib/unbuilder.py +81 -0
  17. fontTools/config/__init__.py +90 -0
  18. fontTools/cu2qu/__init__.py +15 -0
  19. fontTools/cu2qu/__main__.py +6 -0
  20. fontTools/cu2qu/benchmark.py +54 -0
  21. fontTools/cu2qu/cli.py +198 -0
  22. fontTools/cu2qu/cu2qu.c +15545 -0
  23. fontTools/cu2qu/cu2qu.cpython-313-aarch64-linux-gnu.so +0 -0
  24. fontTools/cu2qu/cu2qu.py +531 -0
  25. fontTools/cu2qu/errors.py +77 -0
  26. fontTools/cu2qu/ufo.py +349 -0
  27. fontTools/designspaceLib/__init__.py +3338 -0
  28. fontTools/designspaceLib/__main__.py +6 -0
  29. fontTools/designspaceLib/split.py +475 -0
  30. fontTools/designspaceLib/statNames.py +260 -0
  31. fontTools/designspaceLib/types.py +147 -0
  32. fontTools/encodings/MacRoman.py +258 -0
  33. fontTools/encodings/StandardEncoding.py +258 -0
  34. fontTools/encodings/__init__.py +1 -0
  35. fontTools/encodings/codecs.py +135 -0
  36. fontTools/feaLib/__init__.py +4 -0
  37. fontTools/feaLib/__main__.py +78 -0
  38. fontTools/feaLib/ast.py +2142 -0
  39. fontTools/feaLib/builder.py +1796 -0
  40. fontTools/feaLib/error.py +22 -0
  41. fontTools/feaLib/lexer.c +17336 -0
  42. fontTools/feaLib/lexer.cpython-313-aarch64-linux-gnu.so +0 -0
  43. fontTools/feaLib/lexer.py +287 -0
  44. fontTools/feaLib/location.py +12 -0
  45. fontTools/feaLib/lookupDebugInfo.py +12 -0
  46. fontTools/feaLib/parser.py +2379 -0
  47. fontTools/feaLib/variableScalar.py +113 -0
  48. fontTools/fontBuilder.py +1014 -0
  49. fontTools/help.py +36 -0
  50. fontTools/merge/__init__.py +248 -0
  51. fontTools/merge/__main__.py +6 -0
  52. fontTools/merge/base.py +81 -0
  53. fontTools/merge/cmap.py +173 -0
  54. fontTools/merge/layout.py +526 -0
  55. fontTools/merge/options.py +85 -0
  56. fontTools/merge/tables.py +352 -0
  57. fontTools/merge/unicode.py +78 -0
  58. fontTools/merge/util.py +143 -0
  59. fontTools/misc/__init__.py +1 -0
  60. fontTools/misc/arrayTools.py +424 -0
  61. fontTools/misc/bezierTools.c +40136 -0
  62. fontTools/misc/bezierTools.cpython-313-aarch64-linux-gnu.so +0 -0
  63. fontTools/misc/bezierTools.py +1497 -0
  64. fontTools/misc/classifyTools.py +170 -0
  65. fontTools/misc/cliTools.py +53 -0
  66. fontTools/misc/configTools.py +349 -0
  67. fontTools/misc/cython.py +27 -0
  68. fontTools/misc/dictTools.py +83 -0
  69. fontTools/misc/eexec.py +119 -0
  70. fontTools/misc/encodingTools.py +72 -0
  71. fontTools/misc/etree.py +456 -0
  72. fontTools/misc/filenames.py +245 -0
  73. fontTools/misc/fixedTools.py +253 -0
  74. fontTools/misc/intTools.py +25 -0
  75. fontTools/misc/iterTools.py +12 -0
  76. fontTools/misc/lazyTools.py +42 -0
  77. fontTools/misc/loggingTools.py +543 -0
  78. fontTools/misc/macCreatorType.py +56 -0
  79. fontTools/misc/macRes.py +261 -0
  80. fontTools/misc/plistlib/__init__.py +681 -0
  81. fontTools/misc/plistlib/py.typed +0 -0
  82. fontTools/misc/psCharStrings.py +1496 -0
  83. fontTools/misc/psLib.py +398 -0
  84. fontTools/misc/psOperators.py +572 -0
  85. fontTools/misc/py23.py +96 -0
  86. fontTools/misc/roundTools.py +110 -0
  87. fontTools/misc/sstruct.py +231 -0
  88. fontTools/misc/symfont.py +242 -0
  89. fontTools/misc/testTools.py +233 -0
  90. fontTools/misc/textTools.py +154 -0
  91. fontTools/misc/timeTools.py +88 -0
  92. fontTools/misc/transform.py +516 -0
  93. fontTools/misc/treeTools.py +45 -0
  94. fontTools/misc/vector.py +147 -0
  95. fontTools/misc/visitor.py +142 -0
  96. fontTools/misc/xmlReader.py +188 -0
  97. fontTools/misc/xmlWriter.py +204 -0
  98. fontTools/mtiLib/__init__.py +1400 -0
  99. fontTools/mtiLib/__main__.py +5 -0
  100. fontTools/otlLib/__init__.py +1 -0
  101. fontTools/otlLib/builder.py +3435 -0
  102. fontTools/otlLib/error.py +11 -0
  103. fontTools/otlLib/maxContextCalc.py +96 -0
  104. fontTools/otlLib/optimize/__init__.py +53 -0
  105. fontTools/otlLib/optimize/__main__.py +6 -0
  106. fontTools/otlLib/optimize/gpos.py +439 -0
  107. fontTools/pens/__init__.py +1 -0
  108. fontTools/pens/areaPen.py +52 -0
  109. fontTools/pens/basePen.py +475 -0
  110. fontTools/pens/boundsPen.py +98 -0
  111. fontTools/pens/cairoPen.py +26 -0
  112. fontTools/pens/cocoaPen.py +26 -0
  113. fontTools/pens/cu2quPen.py +325 -0
  114. fontTools/pens/explicitClosingLinePen.py +101 -0
  115. fontTools/pens/filterPen.py +241 -0
  116. fontTools/pens/freetypePen.py +462 -0
  117. fontTools/pens/hashPointPen.py +89 -0
  118. fontTools/pens/momentsPen.c +13459 -0
  119. fontTools/pens/momentsPen.cpython-313-aarch64-linux-gnu.so +0 -0
  120. fontTools/pens/momentsPen.py +879 -0
  121. fontTools/pens/perimeterPen.py +69 -0
  122. fontTools/pens/pointInsidePen.py +192 -0
  123. fontTools/pens/pointPen.py +609 -0
  124. fontTools/pens/qtPen.py +29 -0
  125. fontTools/pens/qu2cuPen.py +105 -0
  126. fontTools/pens/quartzPen.py +43 -0
  127. fontTools/pens/recordingPen.py +335 -0
  128. fontTools/pens/reportLabPen.py +79 -0
  129. fontTools/pens/reverseContourPen.py +96 -0
  130. fontTools/pens/roundingPen.py +130 -0
  131. fontTools/pens/statisticsPen.py +312 -0
  132. fontTools/pens/svgPathPen.py +310 -0
  133. fontTools/pens/t2CharStringPen.py +88 -0
  134. fontTools/pens/teePen.py +55 -0
  135. fontTools/pens/transformPen.py +115 -0
  136. fontTools/pens/ttGlyphPen.py +335 -0
  137. fontTools/pens/wxPen.py +29 -0
  138. fontTools/qu2cu/__init__.py +15 -0
  139. fontTools/qu2cu/__main__.py +7 -0
  140. fontTools/qu2cu/benchmark.py +56 -0
  141. fontTools/qu2cu/cli.py +125 -0
  142. fontTools/qu2cu/qu2cu.c +16738 -0
  143. fontTools/qu2cu/qu2cu.cpython-313-aarch64-linux-gnu.so +0 -0
  144. fontTools/qu2cu/qu2cu.py +405 -0
  145. fontTools/subset/__init__.py +3929 -0
  146. fontTools/subset/__main__.py +6 -0
  147. fontTools/subset/cff.py +184 -0
  148. fontTools/subset/svg.py +253 -0
  149. fontTools/subset/util.py +25 -0
  150. fontTools/svgLib/__init__.py +3 -0
  151. fontTools/svgLib/path/__init__.py +65 -0
  152. fontTools/svgLib/path/arc.py +154 -0
  153. fontTools/svgLib/path/parser.py +322 -0
  154. fontTools/svgLib/path/shapes.py +183 -0
  155. fontTools/t1Lib/__init__.py +648 -0
  156. fontTools/tfmLib.py +460 -0
  157. fontTools/ttLib/__init__.py +30 -0
  158. fontTools/ttLib/__main__.py +148 -0
  159. fontTools/ttLib/macUtils.py +54 -0
  160. fontTools/ttLib/removeOverlaps.py +393 -0
  161. fontTools/ttLib/reorderGlyphs.py +285 -0
  162. fontTools/ttLib/scaleUpem.py +436 -0
  163. fontTools/ttLib/sfnt.py +662 -0
  164. fontTools/ttLib/standardGlyphOrder.py +271 -0
  165. fontTools/ttLib/tables/B_A_S_E_.py +14 -0
  166. fontTools/ttLib/tables/BitmapGlyphMetrics.py +64 -0
  167. fontTools/ttLib/tables/C_B_D_T_.py +113 -0
  168. fontTools/ttLib/tables/C_B_L_C_.py +19 -0
  169. fontTools/ttLib/tables/C_F_F_.py +61 -0
  170. fontTools/ttLib/tables/C_F_F__2.py +26 -0
  171. fontTools/ttLib/tables/C_O_L_R_.py +165 -0
  172. fontTools/ttLib/tables/C_P_A_L_.py +305 -0
  173. fontTools/ttLib/tables/D_S_I_G_.py +158 -0
  174. fontTools/ttLib/tables/D__e_b_g.py +35 -0
  175. fontTools/ttLib/tables/DefaultTable.py +49 -0
  176. fontTools/ttLib/tables/E_B_D_T_.py +835 -0
  177. fontTools/ttLib/tables/E_B_L_C_.py +718 -0
  178. fontTools/ttLib/tables/F_F_T_M_.py +52 -0
  179. fontTools/ttLib/tables/F__e_a_t.py +149 -0
  180. fontTools/ttLib/tables/G_D_E_F_.py +13 -0
  181. fontTools/ttLib/tables/G_M_A_P_.py +148 -0
  182. fontTools/ttLib/tables/G_P_K_G_.py +133 -0
  183. fontTools/ttLib/tables/G_P_O_S_.py +14 -0
  184. fontTools/ttLib/tables/G_S_U_B_.py +13 -0
  185. fontTools/ttLib/tables/G_V_A_R_.py +5 -0
  186. fontTools/ttLib/tables/G__l_a_t.py +235 -0
  187. fontTools/ttLib/tables/G__l_o_c.py +85 -0
  188. fontTools/ttLib/tables/H_V_A_R_.py +13 -0
  189. fontTools/ttLib/tables/J_S_T_F_.py +13 -0
  190. fontTools/ttLib/tables/L_T_S_H_.py +58 -0
  191. fontTools/ttLib/tables/M_A_T_H_.py +13 -0
  192. fontTools/ttLib/tables/M_E_T_A_.py +352 -0
  193. fontTools/ttLib/tables/M_V_A_R_.py +13 -0
  194. fontTools/ttLib/tables/O_S_2f_2.py +752 -0
  195. fontTools/ttLib/tables/S_I_N_G_.py +99 -0
  196. fontTools/ttLib/tables/S_T_A_T_.py +15 -0
  197. fontTools/ttLib/tables/S_V_G_.py +223 -0
  198. fontTools/ttLib/tables/S__i_l_f.py +1040 -0
  199. fontTools/ttLib/tables/S__i_l_l.py +92 -0
  200. fontTools/ttLib/tables/T_S_I_B_.py +13 -0
  201. fontTools/ttLib/tables/T_S_I_C_.py +14 -0
  202. fontTools/ttLib/tables/T_S_I_D_.py +13 -0
  203. fontTools/ttLib/tables/T_S_I_J_.py +13 -0
  204. fontTools/ttLib/tables/T_S_I_P_.py +13 -0
  205. fontTools/ttLib/tables/T_S_I_S_.py +13 -0
  206. fontTools/ttLib/tables/T_S_I_V_.py +26 -0
  207. fontTools/ttLib/tables/T_S_I__0.py +70 -0
  208. fontTools/ttLib/tables/T_S_I__1.py +166 -0
  209. fontTools/ttLib/tables/T_S_I__2.py +17 -0
  210. fontTools/ttLib/tables/T_S_I__3.py +22 -0
  211. fontTools/ttLib/tables/T_S_I__5.py +60 -0
  212. fontTools/ttLib/tables/T_T_F_A_.py +14 -0
  213. fontTools/ttLib/tables/TupleVariation.py +884 -0
  214. fontTools/ttLib/tables/V_A_R_C_.py +12 -0
  215. fontTools/ttLib/tables/V_D_M_X_.py +249 -0
  216. fontTools/ttLib/tables/V_O_R_G_.py +165 -0
  217. fontTools/ttLib/tables/V_V_A_R_.py +13 -0
  218. fontTools/ttLib/tables/__init__.py +98 -0
  219. fontTools/ttLib/tables/_a_n_k_r.py +15 -0
  220. fontTools/ttLib/tables/_a_v_a_r.py +191 -0
  221. fontTools/ttLib/tables/_b_s_l_n.py +15 -0
  222. fontTools/ttLib/tables/_c_i_d_g.py +24 -0
  223. fontTools/ttLib/tables/_c_m_a_p.py +1591 -0
  224. fontTools/ttLib/tables/_c_v_a_r.py +94 -0
  225. fontTools/ttLib/tables/_c_v_t.py +57 -0
  226. fontTools/ttLib/tables/_f_e_a_t.py +15 -0
  227. fontTools/ttLib/tables/_f_p_g_m.py +62 -0
  228. fontTools/ttLib/tables/_f_v_a_r.py +261 -0
  229. fontTools/ttLib/tables/_g_a_s_p.py +63 -0
  230. fontTools/ttLib/tables/_g_c_i_d.py +13 -0
  231. fontTools/ttLib/tables/_g_l_y_f.py +2312 -0
  232. fontTools/ttLib/tables/_g_v_a_r.py +337 -0
  233. fontTools/ttLib/tables/_h_d_m_x.py +127 -0
  234. fontTools/ttLib/tables/_h_e_a_d.py +130 -0
  235. fontTools/ttLib/tables/_h_h_e_a.py +147 -0
  236. fontTools/ttLib/tables/_h_m_t_x.py +160 -0
  237. fontTools/ttLib/tables/_k_e_r_n.py +289 -0
  238. fontTools/ttLib/tables/_l_c_a_r.py +13 -0
  239. fontTools/ttLib/tables/_l_o_c_a.py +70 -0
  240. fontTools/ttLib/tables/_l_t_a_g.py +72 -0
  241. fontTools/ttLib/tables/_m_a_x_p.py +147 -0
  242. fontTools/ttLib/tables/_m_e_t_a.py +112 -0
  243. fontTools/ttLib/tables/_m_o_r_t.py +14 -0
  244. fontTools/ttLib/tables/_m_o_r_x.py +15 -0
  245. fontTools/ttLib/tables/_n_a_m_e.py +1237 -0
  246. fontTools/ttLib/tables/_o_p_b_d.py +14 -0
  247. fontTools/ttLib/tables/_p_o_s_t.py +320 -0
  248. fontTools/ttLib/tables/_p_r_e_p.py +16 -0
  249. fontTools/ttLib/tables/_p_r_o_p.py +12 -0
  250. fontTools/ttLib/tables/_s_b_i_x.py +129 -0
  251. fontTools/ttLib/tables/_t_r_a_k.py +332 -0
  252. fontTools/ttLib/tables/_v_h_e_a.py +139 -0
  253. fontTools/ttLib/tables/_v_m_t_x.py +19 -0
  254. fontTools/ttLib/tables/asciiTable.py +20 -0
  255. fontTools/ttLib/tables/grUtils.py +92 -0
  256. fontTools/ttLib/tables/otBase.py +1466 -0
  257. fontTools/ttLib/tables/otConverters.py +2068 -0
  258. fontTools/ttLib/tables/otData.py +6400 -0
  259. fontTools/ttLib/tables/otTables.py +2708 -0
  260. fontTools/ttLib/tables/otTraverse.py +163 -0
  261. fontTools/ttLib/tables/sbixGlyph.py +149 -0
  262. fontTools/ttLib/tables/sbixStrike.py +177 -0
  263. fontTools/ttLib/tables/table_API_readme.txt +91 -0
  264. fontTools/ttLib/tables/ttProgram.py +594 -0
  265. fontTools/ttLib/ttCollection.py +125 -0
  266. fontTools/ttLib/ttFont.py +1157 -0
  267. fontTools/ttLib/ttGlyphSet.py +490 -0
  268. fontTools/ttLib/ttVisitor.py +32 -0
  269. fontTools/ttLib/woff2.py +1683 -0
  270. fontTools/ttx.py +479 -0
  271. fontTools/ufoLib/__init__.py +2477 -0
  272. fontTools/ufoLib/converters.py +398 -0
  273. fontTools/ufoLib/errors.py +30 -0
  274. fontTools/ufoLib/etree.py +6 -0
  275. fontTools/ufoLib/filenames.py +346 -0
  276. fontTools/ufoLib/glifLib.py +2029 -0
  277. fontTools/ufoLib/kerning.py +121 -0
  278. fontTools/ufoLib/plistlib.py +47 -0
  279. fontTools/ufoLib/pointPen.py +6 -0
  280. fontTools/ufoLib/utils.py +79 -0
  281. fontTools/ufoLib/validators.py +1186 -0
  282. fontTools/unicode.py +50 -0
  283. fontTools/unicodedata/Blocks.py +801 -0
  284. fontTools/unicodedata/Mirrored.py +446 -0
  285. fontTools/unicodedata/OTTags.py +50 -0
  286. fontTools/unicodedata/ScriptExtensions.py +826 -0
  287. fontTools/unicodedata/Scripts.py +3617 -0
  288. fontTools/unicodedata/__init__.py +302 -0
  289. fontTools/varLib/__init__.py +1517 -0
  290. fontTools/varLib/__main__.py +6 -0
  291. fontTools/varLib/avar.py +260 -0
  292. fontTools/varLib/avarPlanner.py +1004 -0
  293. fontTools/varLib/builder.py +215 -0
  294. fontTools/varLib/cff.py +631 -0
  295. fontTools/varLib/errors.py +219 -0
  296. fontTools/varLib/featureVars.py +695 -0
  297. fontTools/varLib/hvar.py +113 -0
  298. fontTools/varLib/instancer/__init__.py +1946 -0
  299. fontTools/varLib/instancer/__main__.py +5 -0
  300. fontTools/varLib/instancer/featureVars.py +190 -0
  301. fontTools/varLib/instancer/names.py +388 -0
  302. fontTools/varLib/instancer/solver.py +309 -0
  303. fontTools/varLib/interpolatable.py +1209 -0
  304. fontTools/varLib/interpolatableHelpers.py +396 -0
  305. fontTools/varLib/interpolatablePlot.py +1269 -0
  306. fontTools/varLib/interpolatableTestContourOrder.py +82 -0
  307. fontTools/varLib/interpolatableTestStartingPoint.py +107 -0
  308. fontTools/varLib/interpolate_layout.py +124 -0
  309. fontTools/varLib/iup.c +19830 -0
  310. fontTools/varLib/iup.cpython-313-aarch64-linux-gnu.so +0 -0
  311. fontTools/varLib/iup.py +490 -0
  312. fontTools/varLib/merger.py +1717 -0
  313. fontTools/varLib/models.py +642 -0
  314. fontTools/varLib/multiVarStore.py +253 -0
  315. fontTools/varLib/mutator.py +518 -0
  316. fontTools/varLib/mvar.py +40 -0
  317. fontTools/varLib/plot.py +238 -0
  318. fontTools/varLib/stat.py +149 -0
  319. fontTools/varLib/varStore.py +739 -0
  320. fontTools/voltLib/__init__.py +5 -0
  321. fontTools/voltLib/__main__.py +206 -0
  322. fontTools/voltLib/ast.py +452 -0
  323. fontTools/voltLib/error.py +12 -0
  324. fontTools/voltLib/lexer.py +99 -0
  325. fontTools/voltLib/parser.py +664 -0
  326. fontTools/voltLib/voltToFea.py +911 -0
  327. fonttools-4.58.3.data/data/share/man/man1/ttx.1 +225 -0
  328. fonttools-4.58.3.dist-info/METADATA +2133 -0
  329. fonttools-4.58.3.dist-info/RECORD +334 -0
  330. fonttools-4.58.3.dist-info/WHEEL +7 -0
  331. fonttools-4.58.3.dist-info/entry_points.txt +5 -0
  332. fonttools-4.58.3.dist-info/licenses/LICENSE +21 -0
  333. fonttools-4.58.3.dist-info/licenses/LICENSE.external +359 -0
  334. fonttools-4.58.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1186 @@
1
+ """Various low level data validators."""
2
+
3
+ import calendar
4
+ from io import open
5
+ import fs.base
6
+ import fs.osfs
7
+
8
+ from collections.abc import Mapping
9
+ from fontTools.ufoLib.utils import numberTypes
10
+
11
+
12
+ # -------
13
+ # Generic
14
+ # -------
15
+
16
+
17
+ def isDictEnough(value):
18
+ """
19
+ Some objects will likely come in that aren't
20
+ dicts but are dict-ish enough.
21
+ """
22
+ if isinstance(value, Mapping):
23
+ return True
24
+ for attr in ("keys", "values", "items"):
25
+ if not hasattr(value, attr):
26
+ return False
27
+ return True
28
+
29
+
30
+ def genericTypeValidator(value, typ):
31
+ """
32
+ Generic. (Added at version 2.)
33
+ """
34
+ return isinstance(value, typ)
35
+
36
+
37
+ def genericIntListValidator(values, validValues):
38
+ """
39
+ Generic. (Added at version 2.)
40
+ """
41
+ if not isinstance(values, (list, tuple)):
42
+ return False
43
+ valuesSet = set(values)
44
+ validValuesSet = set(validValues)
45
+ if valuesSet - validValuesSet:
46
+ return False
47
+ for value in values:
48
+ if not isinstance(value, int):
49
+ return False
50
+ return True
51
+
52
+
53
+ def genericNonNegativeIntValidator(value):
54
+ """
55
+ Generic. (Added at version 3.)
56
+ """
57
+ if not isinstance(value, int):
58
+ return False
59
+ if value < 0:
60
+ return False
61
+ return True
62
+
63
+
64
+ def genericNonNegativeNumberValidator(value):
65
+ """
66
+ Generic. (Added at version 3.)
67
+ """
68
+ if not isinstance(value, numberTypes):
69
+ return False
70
+ if value < 0:
71
+ return False
72
+ return True
73
+
74
+
75
+ def genericDictValidator(value, prototype):
76
+ """
77
+ Generic. (Added at version 3.)
78
+ """
79
+ # not a dict
80
+ if not isinstance(value, Mapping):
81
+ return False
82
+ # missing required keys
83
+ for key, (typ, required) in prototype.items():
84
+ if not required:
85
+ continue
86
+ if key not in value:
87
+ return False
88
+ # unknown keys
89
+ for key in value.keys():
90
+ if key not in prototype:
91
+ return False
92
+ # incorrect types
93
+ for key, v in value.items():
94
+ prototypeType, required = prototype[key]
95
+ if v is None and not required:
96
+ continue
97
+ if not isinstance(v, prototypeType):
98
+ return False
99
+ return True
100
+
101
+
102
+ # --------------
103
+ # fontinfo.plist
104
+ # --------------
105
+
106
+ # Data Validators
107
+
108
+
109
+ def fontInfoStyleMapStyleNameValidator(value):
110
+ """
111
+ Version 2+.
112
+ """
113
+ options = ["regular", "italic", "bold", "bold italic"]
114
+ return value in options
115
+
116
+
117
+ def fontInfoOpenTypeGaspRangeRecordsValidator(value):
118
+ """
119
+ Version 3+.
120
+ """
121
+ if not isinstance(value, list):
122
+ return False
123
+ if len(value) == 0:
124
+ return True
125
+ validBehaviors = [0, 1, 2, 3]
126
+ dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True))
127
+ ppemOrder = []
128
+ for rangeRecord in value:
129
+ if not genericDictValidator(rangeRecord, dictPrototype):
130
+ return False
131
+ ppem = rangeRecord["rangeMaxPPEM"]
132
+ behavior = rangeRecord["rangeGaspBehavior"]
133
+ ppemValidity = genericNonNegativeIntValidator(ppem)
134
+ if not ppemValidity:
135
+ return False
136
+ behaviorValidity = genericIntListValidator(behavior, validBehaviors)
137
+ if not behaviorValidity:
138
+ return False
139
+ ppemOrder.append(ppem)
140
+ if ppemOrder != sorted(ppemOrder):
141
+ return False
142
+ return True
143
+
144
+
145
+ def fontInfoOpenTypeHeadCreatedValidator(value):
146
+ """
147
+ Version 2+.
148
+ """
149
+ # format: 0000/00/00 00:00:00
150
+ if not isinstance(value, str):
151
+ return False
152
+ # basic formatting
153
+ if not len(value) == 19:
154
+ return False
155
+ if value.count(" ") != 1:
156
+ return False
157
+ date, time = value.split(" ")
158
+ if date.count("/") != 2:
159
+ return False
160
+ if time.count(":") != 2:
161
+ return False
162
+ # date
163
+ year, month, day = date.split("/")
164
+ if len(year) != 4:
165
+ return False
166
+ if len(month) != 2:
167
+ return False
168
+ if len(day) != 2:
169
+ return False
170
+ try:
171
+ year = int(year)
172
+ month = int(month)
173
+ day = int(day)
174
+ except ValueError:
175
+ return False
176
+ if month < 1 or month > 12:
177
+ return False
178
+ monthMaxDay = calendar.monthrange(year, month)[1]
179
+ if day < 1 or day > monthMaxDay:
180
+ return False
181
+ # time
182
+ hour, minute, second = time.split(":")
183
+ if len(hour) != 2:
184
+ return False
185
+ if len(minute) != 2:
186
+ return False
187
+ if len(second) != 2:
188
+ return False
189
+ try:
190
+ hour = int(hour)
191
+ minute = int(minute)
192
+ second = int(second)
193
+ except ValueError:
194
+ return False
195
+ if hour < 0 or hour > 23:
196
+ return False
197
+ if minute < 0 or minute > 59:
198
+ return False
199
+ if second < 0 or second > 59:
200
+ return False
201
+ # fallback
202
+ return True
203
+
204
+
205
+ def fontInfoOpenTypeNameRecordsValidator(value):
206
+ """
207
+ Version 3+.
208
+ """
209
+ if not isinstance(value, list):
210
+ return False
211
+ dictPrototype = dict(
212
+ nameID=(int, True),
213
+ platformID=(int, True),
214
+ encodingID=(int, True),
215
+ languageID=(int, True),
216
+ string=(str, True),
217
+ )
218
+ for nameRecord in value:
219
+ if not genericDictValidator(nameRecord, dictPrototype):
220
+ return False
221
+ return True
222
+
223
+
224
+ def fontInfoOpenTypeOS2WeightClassValidator(value):
225
+ """
226
+ Version 2+.
227
+ """
228
+ if not isinstance(value, int):
229
+ return False
230
+ if value < 0:
231
+ return False
232
+ return True
233
+
234
+
235
+ def fontInfoOpenTypeOS2WidthClassValidator(value):
236
+ """
237
+ Version 2+.
238
+ """
239
+ if not isinstance(value, int):
240
+ return False
241
+ if value < 1:
242
+ return False
243
+ if value > 9:
244
+ return False
245
+ return True
246
+
247
+
248
+ def fontInfoVersion2OpenTypeOS2PanoseValidator(values):
249
+ """
250
+ Version 2.
251
+ """
252
+ if not isinstance(values, (list, tuple)):
253
+ return False
254
+ if len(values) != 10:
255
+ return False
256
+ for value in values:
257
+ if not isinstance(value, int):
258
+ return False
259
+ # XXX further validation?
260
+ return True
261
+
262
+
263
+ def fontInfoVersion3OpenTypeOS2PanoseValidator(values):
264
+ """
265
+ Version 3+.
266
+ """
267
+ if not isinstance(values, (list, tuple)):
268
+ return False
269
+ if len(values) != 10:
270
+ return False
271
+ for value in values:
272
+ if not isinstance(value, int):
273
+ return False
274
+ if value < 0:
275
+ return False
276
+ # XXX further validation?
277
+ return True
278
+
279
+
280
+ def fontInfoOpenTypeOS2FamilyClassValidator(values):
281
+ """
282
+ Version 2+.
283
+ """
284
+ if not isinstance(values, (list, tuple)):
285
+ return False
286
+ if len(values) != 2:
287
+ return False
288
+ for value in values:
289
+ if not isinstance(value, int):
290
+ return False
291
+ classID, subclassID = values
292
+ if classID < 0 or classID > 14:
293
+ return False
294
+ if subclassID < 0 or subclassID > 15:
295
+ return False
296
+ return True
297
+
298
+
299
+ def fontInfoPostscriptBluesValidator(values):
300
+ """
301
+ Version 2+.
302
+ """
303
+ if not isinstance(values, (list, tuple)):
304
+ return False
305
+ if len(values) > 14:
306
+ return False
307
+ if len(values) % 2:
308
+ return False
309
+ for value in values:
310
+ if not isinstance(value, numberTypes):
311
+ return False
312
+ return True
313
+
314
+
315
+ def fontInfoPostscriptOtherBluesValidator(values):
316
+ """
317
+ Version 2+.
318
+ """
319
+ if not isinstance(values, (list, tuple)):
320
+ return False
321
+ if len(values) > 10:
322
+ return False
323
+ if len(values) % 2:
324
+ return False
325
+ for value in values:
326
+ if not isinstance(value, numberTypes):
327
+ return False
328
+ return True
329
+
330
+
331
+ def fontInfoPostscriptStemsValidator(values):
332
+ """
333
+ Version 2+.
334
+ """
335
+ if not isinstance(values, (list, tuple)):
336
+ return False
337
+ if len(values) > 12:
338
+ return False
339
+ for value in values:
340
+ if not isinstance(value, numberTypes):
341
+ return False
342
+ return True
343
+
344
+
345
+ def fontInfoPostscriptWindowsCharacterSetValidator(value):
346
+ """
347
+ Version 2+.
348
+ """
349
+ validValues = list(range(1, 21))
350
+ if value not in validValues:
351
+ return False
352
+ return True
353
+
354
+
355
+ def fontInfoWOFFMetadataUniqueIDValidator(value):
356
+ """
357
+ Version 3+.
358
+ """
359
+ dictPrototype = dict(id=(str, True))
360
+ if not genericDictValidator(value, dictPrototype):
361
+ return False
362
+ return True
363
+
364
+
365
+ def fontInfoWOFFMetadataVendorValidator(value):
366
+ """
367
+ Version 3+.
368
+ """
369
+ dictPrototype = {
370
+ "name": (str, True),
371
+ "url": (str, False),
372
+ "dir": (str, False),
373
+ "class": (str, False),
374
+ }
375
+ if not genericDictValidator(value, dictPrototype):
376
+ return False
377
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
378
+ return False
379
+ return True
380
+
381
+
382
+ def fontInfoWOFFMetadataCreditsValidator(value):
383
+ """
384
+ Version 3+.
385
+ """
386
+ dictPrototype = dict(credits=(list, True))
387
+ if not genericDictValidator(value, dictPrototype):
388
+ return False
389
+ if not len(value["credits"]):
390
+ return False
391
+ dictPrototype = {
392
+ "name": (str, True),
393
+ "url": (str, False),
394
+ "role": (str, False),
395
+ "dir": (str, False),
396
+ "class": (str, False),
397
+ }
398
+ for credit in value["credits"]:
399
+ if not genericDictValidator(credit, dictPrototype):
400
+ return False
401
+ if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
402
+ return False
403
+ return True
404
+
405
+
406
+ def fontInfoWOFFMetadataDescriptionValidator(value):
407
+ """
408
+ Version 3+.
409
+ """
410
+ dictPrototype = dict(url=(str, False), text=(list, True))
411
+ if not genericDictValidator(value, dictPrototype):
412
+ return False
413
+ for text in value["text"]:
414
+ if not fontInfoWOFFMetadataTextValue(text):
415
+ return False
416
+ return True
417
+
418
+
419
+ def fontInfoWOFFMetadataLicenseValidator(value):
420
+ """
421
+ Version 3+.
422
+ """
423
+ dictPrototype = dict(url=(str, False), text=(list, False), id=(str, False))
424
+ if not genericDictValidator(value, dictPrototype):
425
+ return False
426
+ if "text" in value:
427
+ for text in value["text"]:
428
+ if not fontInfoWOFFMetadataTextValue(text):
429
+ return False
430
+ return True
431
+
432
+
433
+ def fontInfoWOFFMetadataTrademarkValidator(value):
434
+ """
435
+ Version 3+.
436
+ """
437
+ dictPrototype = dict(text=(list, True))
438
+ if not genericDictValidator(value, dictPrototype):
439
+ return False
440
+ for text in value["text"]:
441
+ if not fontInfoWOFFMetadataTextValue(text):
442
+ return False
443
+ return True
444
+
445
+
446
+ def fontInfoWOFFMetadataCopyrightValidator(value):
447
+ """
448
+ Version 3+.
449
+ """
450
+ dictPrototype = dict(text=(list, True))
451
+ if not genericDictValidator(value, dictPrototype):
452
+ return False
453
+ for text in value["text"]:
454
+ if not fontInfoWOFFMetadataTextValue(text):
455
+ return False
456
+ return True
457
+
458
+
459
+ def fontInfoWOFFMetadataLicenseeValidator(value):
460
+ """
461
+ Version 3+.
462
+ """
463
+ dictPrototype = {"name": (str, True), "dir": (str, False), "class": (str, False)}
464
+ if not genericDictValidator(value, dictPrototype):
465
+ return False
466
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
467
+ return False
468
+ return True
469
+
470
+
471
+ def fontInfoWOFFMetadataTextValue(value):
472
+ """
473
+ Version 3+.
474
+ """
475
+ dictPrototype = {
476
+ "text": (str, True),
477
+ "language": (str, False),
478
+ "dir": (str, False),
479
+ "class": (str, False),
480
+ }
481
+ if not genericDictValidator(value, dictPrototype):
482
+ return False
483
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
484
+ return False
485
+ return True
486
+
487
+
488
+ def fontInfoWOFFMetadataExtensionsValidator(value):
489
+ """
490
+ Version 3+.
491
+ """
492
+ if not isinstance(value, list):
493
+ return False
494
+ if not value:
495
+ return False
496
+ for extension in value:
497
+ if not fontInfoWOFFMetadataExtensionValidator(extension):
498
+ return False
499
+ return True
500
+
501
+
502
+ def fontInfoWOFFMetadataExtensionValidator(value):
503
+ """
504
+ Version 3+.
505
+ """
506
+ dictPrototype = dict(names=(list, False), items=(list, True), id=(str, False))
507
+ if not genericDictValidator(value, dictPrototype):
508
+ return False
509
+ if "names" in value:
510
+ for name in value["names"]:
511
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
512
+ return False
513
+ for item in value["items"]:
514
+ if not fontInfoWOFFMetadataExtensionItemValidator(item):
515
+ return False
516
+ return True
517
+
518
+
519
+ def fontInfoWOFFMetadataExtensionItemValidator(value):
520
+ """
521
+ Version 3+.
522
+ """
523
+ dictPrototype = dict(id=(str, False), names=(list, True), values=(list, True))
524
+ if not genericDictValidator(value, dictPrototype):
525
+ return False
526
+ for name in value["names"]:
527
+ if not fontInfoWOFFMetadataExtensionNameValidator(name):
528
+ return False
529
+ for val in value["values"]:
530
+ if not fontInfoWOFFMetadataExtensionValueValidator(val):
531
+ return False
532
+ return True
533
+
534
+
535
+ def fontInfoWOFFMetadataExtensionNameValidator(value):
536
+ """
537
+ Version 3+.
538
+ """
539
+ dictPrototype = {
540
+ "text": (str, True),
541
+ "language": (str, False),
542
+ "dir": (str, False),
543
+ "class": (str, False),
544
+ }
545
+ if not genericDictValidator(value, dictPrototype):
546
+ return False
547
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
548
+ return False
549
+ return True
550
+
551
+
552
+ def fontInfoWOFFMetadataExtensionValueValidator(value):
553
+ """
554
+ Version 3+.
555
+ """
556
+ dictPrototype = {
557
+ "text": (str, True),
558
+ "language": (str, False),
559
+ "dir": (str, False),
560
+ "class": (str, False),
561
+ }
562
+ if not genericDictValidator(value, dictPrototype):
563
+ return False
564
+ if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
565
+ return False
566
+ return True
567
+
568
+
569
+ # ----------
570
+ # Guidelines
571
+ # ----------
572
+
573
+
574
+ def guidelinesValidator(value, identifiers=None):
575
+ """
576
+ Version 3+.
577
+ """
578
+ if not isinstance(value, list):
579
+ return False
580
+ if identifiers is None:
581
+ identifiers = set()
582
+ for guide in value:
583
+ if not guidelineValidator(guide):
584
+ return False
585
+ identifier = guide.get("identifier")
586
+ if identifier is not None:
587
+ if identifier in identifiers:
588
+ return False
589
+ identifiers.add(identifier)
590
+ return True
591
+
592
+
593
+ _guidelineDictPrototype = dict(
594
+ x=((int, float), False),
595
+ y=((int, float), False),
596
+ angle=((int, float), False),
597
+ name=(str, False),
598
+ color=(str, False),
599
+ identifier=(str, False),
600
+ )
601
+
602
+
603
+ def guidelineValidator(value):
604
+ """
605
+ Version 3+.
606
+ """
607
+ if not genericDictValidator(value, _guidelineDictPrototype):
608
+ return False
609
+ x = value.get("x")
610
+ y = value.get("y")
611
+ angle = value.get("angle")
612
+ # x or y must be present
613
+ if x is None and y is None:
614
+ return False
615
+ # if x or y are None, angle must not be present
616
+ if x is None or y is None:
617
+ if angle is not None:
618
+ return False
619
+ # if x and y are defined, angle must be defined
620
+ if x is not None and y is not None and angle is None:
621
+ return False
622
+ # angle must be between 0 and 360
623
+ if angle is not None:
624
+ if angle < 0:
625
+ return False
626
+ if angle > 360:
627
+ return False
628
+ # identifier must be 1 or more characters
629
+ identifier = value.get("identifier")
630
+ if identifier is not None and not identifierValidator(identifier):
631
+ return False
632
+ # color must follow the proper format
633
+ color = value.get("color")
634
+ if color is not None and not colorValidator(color):
635
+ return False
636
+ return True
637
+
638
+
639
+ # -------
640
+ # Anchors
641
+ # -------
642
+
643
+
644
+ def anchorsValidator(value, identifiers=None):
645
+ """
646
+ Version 3+.
647
+ """
648
+ if not isinstance(value, list):
649
+ return False
650
+ if identifiers is None:
651
+ identifiers = set()
652
+ for anchor in value:
653
+ if not anchorValidator(anchor):
654
+ return False
655
+ identifier = anchor.get("identifier")
656
+ if identifier is not None:
657
+ if identifier in identifiers:
658
+ return False
659
+ identifiers.add(identifier)
660
+ return True
661
+
662
+
663
+ _anchorDictPrototype = dict(
664
+ x=((int, float), False),
665
+ y=((int, float), False),
666
+ name=(str, False),
667
+ color=(str, False),
668
+ identifier=(str, False),
669
+ )
670
+
671
+
672
+ def anchorValidator(value):
673
+ """
674
+ Version 3+.
675
+ """
676
+ if not genericDictValidator(value, _anchorDictPrototype):
677
+ return False
678
+ x = value.get("x")
679
+ y = value.get("y")
680
+ # x and y must be present
681
+ if x is None or y is None:
682
+ return False
683
+ # identifier must be 1 or more characters
684
+ identifier = value.get("identifier")
685
+ if identifier is not None and not identifierValidator(identifier):
686
+ return False
687
+ # color must follow the proper format
688
+ color = value.get("color")
689
+ if color is not None and not colorValidator(color):
690
+ return False
691
+ return True
692
+
693
+
694
+ # ----------
695
+ # Identifier
696
+ # ----------
697
+
698
+
699
+ def identifierValidator(value):
700
+ """
701
+ Version 3+.
702
+
703
+ >>> identifierValidator("a")
704
+ True
705
+ >>> identifierValidator("")
706
+ False
707
+ >>> identifierValidator("a" * 101)
708
+ False
709
+ """
710
+ validCharactersMin = 0x20
711
+ validCharactersMax = 0x7E
712
+ if not isinstance(value, str):
713
+ return False
714
+ if not value:
715
+ return False
716
+ if len(value) > 100:
717
+ return False
718
+ for c in value:
719
+ c = ord(c)
720
+ if c < validCharactersMin or c > validCharactersMax:
721
+ return False
722
+ return True
723
+
724
+
725
+ # -----
726
+ # Color
727
+ # -----
728
+
729
+
730
+ def colorValidator(value):
731
+ """
732
+ Version 3+.
733
+
734
+ >>> colorValidator("0,0,0,0")
735
+ True
736
+ >>> colorValidator(".5,.5,.5,.5")
737
+ True
738
+ >>> colorValidator("0.5,0.5,0.5,0.5")
739
+ True
740
+ >>> colorValidator("1,1,1,1")
741
+ True
742
+
743
+ >>> colorValidator("2,0,0,0")
744
+ False
745
+ >>> colorValidator("0,2,0,0")
746
+ False
747
+ >>> colorValidator("0,0,2,0")
748
+ False
749
+ >>> colorValidator("0,0,0,2")
750
+ False
751
+
752
+ >>> colorValidator("1r,1,1,1")
753
+ False
754
+ >>> colorValidator("1,1g,1,1")
755
+ False
756
+ >>> colorValidator("1,1,1b,1")
757
+ False
758
+ >>> colorValidator("1,1,1,1a")
759
+ False
760
+
761
+ >>> colorValidator("1 1 1 1")
762
+ False
763
+ >>> colorValidator("1 1,1,1")
764
+ False
765
+ >>> colorValidator("1,1 1,1")
766
+ False
767
+ >>> colorValidator("1,1,1 1")
768
+ False
769
+
770
+ >>> colorValidator("1, 1, 1, 1")
771
+ True
772
+ """
773
+ if not isinstance(value, str):
774
+ return False
775
+ parts = value.split(",")
776
+ if len(parts) != 4:
777
+ return False
778
+ for part in parts:
779
+ part = part.strip()
780
+ converted = False
781
+ try:
782
+ part = int(part)
783
+ converted = True
784
+ except ValueError:
785
+ pass
786
+ if not converted:
787
+ try:
788
+ part = float(part)
789
+ converted = True
790
+ except ValueError:
791
+ pass
792
+ if not converted:
793
+ return False
794
+ if part < 0:
795
+ return False
796
+ if part > 1:
797
+ return False
798
+ return True
799
+
800
+
801
+ # -----
802
+ # image
803
+ # -----
804
+
805
+ pngSignature = b"\x89PNG\r\n\x1a\n"
806
+
807
+ _imageDictPrototype = dict(
808
+ fileName=(str, True),
809
+ xScale=((int, float), False),
810
+ xyScale=((int, float), False),
811
+ yxScale=((int, float), False),
812
+ yScale=((int, float), False),
813
+ xOffset=((int, float), False),
814
+ yOffset=((int, float), False),
815
+ color=(str, False),
816
+ )
817
+
818
+
819
+ def imageValidator(value):
820
+ """
821
+ Version 3+.
822
+ """
823
+ if not genericDictValidator(value, _imageDictPrototype):
824
+ return False
825
+ # fileName must be one or more characters
826
+ if not value["fileName"]:
827
+ return False
828
+ # color must follow the proper format
829
+ color = value.get("color")
830
+ if color is not None and not colorValidator(color):
831
+ return False
832
+ return True
833
+
834
+
835
+ def pngValidator(path=None, data=None, fileObj=None):
836
+ """
837
+ Version 3+.
838
+
839
+ This checks the signature of the image data.
840
+ """
841
+ assert path is not None or data is not None or fileObj is not None
842
+ if path is not None:
843
+ with open(path, "rb") as f:
844
+ signature = f.read(8)
845
+ elif data is not None:
846
+ signature = data[:8]
847
+ elif fileObj is not None:
848
+ pos = fileObj.tell()
849
+ signature = fileObj.read(8)
850
+ fileObj.seek(pos)
851
+ if signature != pngSignature:
852
+ return False, "Image does not begin with the PNG signature."
853
+ return True, None
854
+
855
+
856
+ # -------------------
857
+ # layercontents.plist
858
+ # -------------------
859
+
860
+
861
+ def layerContentsValidator(value, ufoPathOrFileSystem):
862
+ """
863
+ Check the validity of layercontents.plist.
864
+ Version 3+.
865
+ """
866
+ if isinstance(ufoPathOrFileSystem, fs.base.FS):
867
+ fileSystem = ufoPathOrFileSystem
868
+ else:
869
+ fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)
870
+
871
+ bogusFileMessage = "layercontents.plist in not in the correct format."
872
+ # file isn't in the right format
873
+ if not isinstance(value, list):
874
+ return False, bogusFileMessage
875
+ # work through each entry
876
+ usedLayerNames = set()
877
+ usedDirectories = set()
878
+ contents = {}
879
+ for entry in value:
880
+ # layer entry in the incorrect format
881
+ if not isinstance(entry, list):
882
+ return False, bogusFileMessage
883
+ if not len(entry) == 2:
884
+ return False, bogusFileMessage
885
+ for i in entry:
886
+ if not isinstance(i, str):
887
+ return False, bogusFileMessage
888
+ layerName, directoryName = entry
889
+ # check directory naming
890
+ if directoryName != "glyphs":
891
+ if not directoryName.startswith("glyphs."):
892
+ return (
893
+ False,
894
+ "Invalid directory name (%s) in layercontents.plist."
895
+ % directoryName,
896
+ )
897
+ if len(layerName) == 0:
898
+ return False, "Empty layer name in layercontents.plist."
899
+ # directory doesn't exist
900
+ if not fileSystem.exists(directoryName):
901
+ return False, "A glyphset does not exist at %s." % directoryName
902
+ # default layer name
903
+ if layerName == "public.default" and directoryName != "glyphs":
904
+ return (
905
+ False,
906
+ "The name public.default is being used by a layer that is not the default.",
907
+ )
908
+ # check usage
909
+ if layerName in usedLayerNames:
910
+ return (
911
+ False,
912
+ "The layer name %s is used by more than one layer." % layerName,
913
+ )
914
+ usedLayerNames.add(layerName)
915
+ if directoryName in usedDirectories:
916
+ return (
917
+ False,
918
+ "The directory %s is used by more than one layer." % directoryName,
919
+ )
920
+ usedDirectories.add(directoryName)
921
+ # store
922
+ contents[layerName] = directoryName
923
+ # missing default layer
924
+ foundDefault = "glyphs" in contents.values()
925
+ if not foundDefault:
926
+ return False, "The required default glyph set is not in the UFO."
927
+ return True, None
928
+
929
+
930
+ # ------------
931
+ # groups.plist
932
+ # ------------
933
+
934
+
935
+ def groupsValidator(value):
936
+ """
937
+ Check the validity of the groups.
938
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
939
+
940
+ >>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
941
+ >>> groupsValidator(groups)
942
+ (True, None)
943
+
944
+ >>> groups = {"" : ["A"]}
945
+ >>> valid, msg = groupsValidator(groups)
946
+ >>> valid
947
+ False
948
+ >>> print(msg)
949
+ A group has an empty name.
950
+
951
+ >>> groups = {"public.awesome" : ["A"]}
952
+ >>> groupsValidator(groups)
953
+ (True, None)
954
+
955
+ >>> groups = {"public.kern1." : ["A"]}
956
+ >>> valid, msg = groupsValidator(groups)
957
+ >>> valid
958
+ False
959
+ >>> print(msg)
960
+ The group data contains a kerning group with an incomplete name.
961
+ >>> groups = {"public.kern2." : ["A"]}
962
+ >>> valid, msg = groupsValidator(groups)
963
+ >>> valid
964
+ False
965
+ >>> print(msg)
966
+ The group data contains a kerning group with an incomplete name.
967
+
968
+ >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
969
+ >>> groupsValidator(groups)
970
+ (True, None)
971
+
972
+ >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
973
+ >>> valid, msg = groupsValidator(groups)
974
+ >>> valid
975
+ False
976
+ >>> print(msg)
977
+ The glyph "A" occurs in too many kerning groups.
978
+ """
979
+ bogusFormatMessage = "The group data is not in the correct format."
980
+ if not isDictEnough(value):
981
+ return False, bogusFormatMessage
982
+ firstSideMapping = {}
983
+ secondSideMapping = {}
984
+ for groupName, glyphList in value.items():
985
+ if not isinstance(groupName, (str)):
986
+ return False, bogusFormatMessage
987
+ if not isinstance(glyphList, (list, tuple)):
988
+ return False, bogusFormatMessage
989
+ if not groupName:
990
+ return False, "A group has an empty name."
991
+ if groupName.startswith("public."):
992
+ if not groupName.startswith("public.kern1.") and not groupName.startswith(
993
+ "public.kern2."
994
+ ):
995
+ # unknown public.* name. silently skip.
996
+ continue
997
+ else:
998
+ if len("public.kernN.") == len(groupName):
999
+ return (
1000
+ False,
1001
+ "The group data contains a kerning group with an incomplete name.",
1002
+ )
1003
+ if groupName.startswith("public.kern1."):
1004
+ d = firstSideMapping
1005
+ else:
1006
+ d = secondSideMapping
1007
+ for glyphName in glyphList:
1008
+ if not isinstance(glyphName, str):
1009
+ return (
1010
+ False,
1011
+ "The group data %s contains an invalid member." % groupName,
1012
+ )
1013
+ if glyphName in d:
1014
+ return (
1015
+ False,
1016
+ 'The glyph "%s" occurs in too many kerning groups.' % glyphName,
1017
+ )
1018
+ d[glyphName] = groupName
1019
+ return True, None
1020
+
1021
+
1022
+ # -------------
1023
+ # kerning.plist
1024
+ # -------------
1025
+
1026
+
1027
+ def kerningValidator(data):
1028
+ """
1029
+ Check the validity of the kerning data structure.
1030
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
1031
+
1032
+ >>> kerning = {"A" : {"B" : 100}}
1033
+ >>> kerningValidator(kerning)
1034
+ (True, None)
1035
+
1036
+ >>> kerning = {"A" : ["B"]}
1037
+ >>> valid, msg = kerningValidator(kerning)
1038
+ >>> valid
1039
+ False
1040
+ >>> print(msg)
1041
+ The kerning data is not in the correct format.
1042
+
1043
+ >>> kerning = {"A" : {"B" : "100"}}
1044
+ >>> valid, msg = kerningValidator(kerning)
1045
+ >>> valid
1046
+ False
1047
+ >>> print(msg)
1048
+ The kerning data is not in the correct format.
1049
+ """
1050
+ bogusFormatMessage = "The kerning data is not in the correct format."
1051
+ if not isinstance(data, Mapping):
1052
+ return False, bogusFormatMessage
1053
+ for first, secondDict in data.items():
1054
+ if not isinstance(first, str):
1055
+ return False, bogusFormatMessage
1056
+ elif not isinstance(secondDict, Mapping):
1057
+ return False, bogusFormatMessage
1058
+ for second, value in secondDict.items():
1059
+ if not isinstance(second, str):
1060
+ return False, bogusFormatMessage
1061
+ elif not isinstance(value, numberTypes):
1062
+ return False, bogusFormatMessage
1063
+ return True, None
1064
+
1065
+
1066
+ # -------------
1067
+ # lib.plist/lib
1068
+ # -------------
1069
+
1070
+ _bogusLibFormatMessage = "The lib data is not in the correct format: %s"
1071
+
1072
+
1073
+ def fontLibValidator(value):
1074
+ """
1075
+ Check the validity of the lib.
1076
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
1077
+
1078
+ >>> lib = {"foo" : "bar"}
1079
+ >>> fontLibValidator(lib)
1080
+ (True, None)
1081
+
1082
+ >>> lib = {"public.awesome" : "hello"}
1083
+ >>> fontLibValidator(lib)
1084
+ (True, None)
1085
+
1086
+ >>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
1087
+ >>> fontLibValidator(lib)
1088
+ (True, None)
1089
+
1090
+ >>> lib = "hello"
1091
+ >>> valid, msg = fontLibValidator(lib)
1092
+ >>> valid
1093
+ False
1094
+ >>> print(msg) # doctest: +ELLIPSIS
1095
+ The lib data is not in the correct format: expected a dictionary, ...
1096
+
1097
+ >>> lib = {1: "hello"}
1098
+ >>> valid, msg = fontLibValidator(lib)
1099
+ >>> valid
1100
+ False
1101
+ >>> print(msg)
1102
+ The lib key is not properly formatted: expected str, found int: 1
1103
+
1104
+ >>> lib = {"public.glyphOrder" : "hello"}
1105
+ >>> valid, msg = fontLibValidator(lib)
1106
+ >>> valid
1107
+ False
1108
+ >>> print(msg) # doctest: +ELLIPSIS
1109
+ public.glyphOrder is not properly formatted: expected list or tuple,...
1110
+
1111
+ >>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
1112
+ >>> valid, msg = fontLibValidator(lib)
1113
+ >>> valid
1114
+ False
1115
+ >>> print(msg) # doctest: +ELLIPSIS
1116
+ public.glyphOrder is not properly formatted: expected str,...
1117
+ """
1118
+ if not isDictEnough(value):
1119
+ reason = "expected a dictionary, found %s" % type(value).__name__
1120
+ return False, _bogusLibFormatMessage % reason
1121
+ for key, value in value.items():
1122
+ if not isinstance(key, str):
1123
+ return False, (
1124
+ "The lib key is not properly formatted: expected str, found %s: %r"
1125
+ % (type(key).__name__, key)
1126
+ )
1127
+ # public.glyphOrder
1128
+ if key == "public.glyphOrder":
1129
+ bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
1130
+ if not isinstance(value, (list, tuple)):
1131
+ reason = "expected list or tuple, found %s" % type(value).__name__
1132
+ return False, bogusGlyphOrderMessage % reason
1133
+ for glyphName in value:
1134
+ if not isinstance(glyphName, str):
1135
+ reason = "expected str, found %s" % type(glyphName).__name__
1136
+ return False, bogusGlyphOrderMessage % reason
1137
+ return True, None
1138
+
1139
+
1140
+ # --------
1141
+ # GLIF lib
1142
+ # --------
1143
+
1144
+
1145
+ def glyphLibValidator(value):
1146
+ """
1147
+ Check the validity of the lib.
1148
+ Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).
1149
+
1150
+ >>> lib = {"foo" : "bar"}
1151
+ >>> glyphLibValidator(lib)
1152
+ (True, None)
1153
+
1154
+ >>> lib = {"public.awesome" : "hello"}
1155
+ >>> glyphLibValidator(lib)
1156
+ (True, None)
1157
+
1158
+ >>> lib = {"public.markColor" : "1,0,0,0.5"}
1159
+ >>> glyphLibValidator(lib)
1160
+ (True, None)
1161
+
1162
+ >>> lib = {"public.markColor" : 1}
1163
+ >>> valid, msg = glyphLibValidator(lib)
1164
+ >>> valid
1165
+ False
1166
+ >>> print(msg)
1167
+ public.markColor is not properly formatted.
1168
+ """
1169
+ if not isDictEnough(value):
1170
+ reason = "expected a dictionary, found %s" % type(value).__name__
1171
+ return False, _bogusLibFormatMessage % reason
1172
+ for key, value in value.items():
1173
+ if not isinstance(key, str):
1174
+ reason = "key (%s) should be a string" % key
1175
+ return False, _bogusLibFormatMessage % reason
1176
+ # public.markColor
1177
+ if key == "public.markColor":
1178
+ if not colorValidator(value):
1179
+ return False, "public.markColor is not properly formatted."
1180
+ return True, None
1181
+
1182
+
1183
+ if __name__ == "__main__":
1184
+ import doctest
1185
+
1186
+ doctest.testmod()