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