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